Compare commits

..

232 Commits

Author SHA1 Message Date
Simon Zolin
7d2df26335 Merge: Bump version to v0.95-hotfix
* commit 'ae403fb13752df1fcdf33839d0747e44722382db':
  Bump version to v0.95-hotfix
2019-04-24 14:39:52 +03:00
Simon Zolin
ae403fb137 Bump version to v0.95-hotfix 2019-04-24 14:38:00 +03:00
Simon Zolin
e1bb89c393 Merge: dnsfilter: prevent recursion when both parental control and safebrowsing are enabled
Close #732

* commit 'c4e67690f4fcceb055cbea73610b5974855db96f':
  * dnsfilter: don't use global variable for custom resolver function
  - dnsfilter: prevent recursion when both parental control and safebrowsing are enabled
2019-04-24 12:52:16 +03:00
Simon Zolin
c4e67690f4 * dnsfilter: don't use global variable for custom resolver function 2019-04-24 12:49:12 +03:00
Simon Zolin
f6023b395e - dnsfilter: prevent recursion when both parental control and safebrowsing are enabled 2019-04-24 12:38:05 +03:00
Simon Zolin
cedab695c2 Merge: Bump version to v0.95
* commit '1c339e5fcd73a4ce070f7284b1ea7d1b44d63803':
  Bump version to v0.95
2019-04-23 20:28:54 +03:00
Simon Zolin
1c339e5fcd Bump version to v0.95 2019-04-23 20:23:39 +03:00
Andrey Meshkov
4eb910c35e Merge pull request #203 in DNS/adguard-dns from fix/647 to master
* commit 'c6957bed64acd5b2265b8b94bb07f8bb77520084':
  - install: immediately schedule filters update procedure after installation is complete
2019-04-23 20:15:12 +03:00
Simon Zolin
c6957bed64 - install: immediately schedule filters update procedure after installation is complete 2019-04-23 19:56:21 +03:00
Andrey Meshkov
8e6f7be5b8 Merge pull request #202 in DNS/adguard-dns from fix/647 to master
* commit '528c1a72cac331f08de5fdee4538529c70dff1bb':
  - use 127.0.0.1 as a resolver address when DNS binding address is 0.0.0.0
  - app: don't print filter update error messages on first launch before  DNS server is set up
2019-04-23 19:48:05 +03:00
Simon Zolin
528c1a72ca - use 127.0.0.1 as a resolver address when DNS binding address is 0.0.0.0 2019-04-23 19:37:14 +03:00
Simon Zolin
3087c54a15 - app: don't print filter update error messages on first launch before
DNS server is set up
2019-04-23 19:26:51 +03:00
Simon Zolin
5c65d0cabe Merge: various bugfixes
* commit '4231920ee84074e573539c05c53cb357b1a22154':
  * client: add "DuckDuckGo" to safesearch label text
  - dhcp: fix build on macos #704
  - dnsfilter: fix npe in dnsfilter test
  - dnsfilter: fix safesearch issue #268
2019-04-23 16:20:46 +03:00
Simon Zolin
4231920ee8 * client: add "DuckDuckGo" to safesearch label text 2019-04-23 15:25:09 +03:00
Simon Zolin
d5f46f51b8 - dhcp: fix build on macos #704 2019-04-23 15:14:26 +03:00
Aleksey Dmitrevskiy
a860c8e6ff - dnsfilter: fix npe in dnsfilter test 2019-04-23 15:09:23 +03:00
Aleksey Dmitrevskiy
0794704f74 - dnsfilter: fix safesearch issue #268 2019-04-23 15:08:41 +03:00
Simon Zolin
173ab2a3c1 Merge: - /control/dhcp/find_active_dhcp: fix DHCP server detection
Close #704

* commit '68dc8a13411c09cd8c382a1bf986953659166506':
  * control: update "DHCP server not found" message
  * update openapi.yaml
  - /control/dhcp/find_active_dhcp: fix DHCP server detection
2019-04-23 12:29:15 +03:00
Simon Zolin
68dc8a1341 * control: update "DHCP server not found" message 2019-04-23 12:26:14 +03:00
Simon Zolin
043e89a1a4 * update openapi.yaml 2019-04-23 12:26:14 +03:00
Simon Zolin
c5ed6da5bd - /control/dhcp/find_active_dhcp: fix DHCP server detection
Before this patch we couldn't receive incoming DHCP packets.
Now we bind() to 0.0.0.0 and set the required network interface
 using SO_BINDTODEVICE option.

As an improvement, we now wait until a reply to our request is
 received and skip all unknown packets.
2019-04-23 12:26:14 +03:00
Simon Zolin
69c5f175e8 Merge: Installation wizard #685
* commit '79b0fac01a544e35207420fed78e1c2a63d428d8':
  * control: move /install handlers to a separate file
  + add technical document
  * app: move code for http server loop to a separate function
  * client: fixed getDefaultAddresses structure
  - client: npm audit fix
  * client: validate form on load
  * client: installation wizard additional checks
  * update openapi.yaml
  + service install: a post-install guide of what to do next
  * control: /install/configure: validate port number
  * control: /install/configure: reset configuration back to its current state on error
  + control: /install/*: test TCP port availability for DNS server
  + control: /install/check_config: Check and deactivate DNSStubListener
  * control: /install/configure: refactor
  + control: add /install/check_config handler
  * control: /install/get_addresses: don't check if ports are available
  + app: unix, windows: require root user on first launch
  * setRlimit(): move OS-specific code to separate files
2019-04-23 12:16:34 +03:00
Simon Zolin
79b0fac01a * control: move /install handlers to a separate file 2019-04-23 11:48:30 +03:00
Simon Zolin
bc81a0ecff + add technical document 2019-04-23 11:45:09 +03:00
Simon Zolin
1ac9419c94 * app: move code for http server loop to a separate function 2019-04-23 11:41:53 +03:00
Ildar Kamalov
5f88abb322 * client: fixed getDefaultAddresses structure 2019-04-23 11:41:53 +03:00
Ildar Kamalov
33db419384 - client: npm audit fix 2019-04-23 11:41:53 +03:00
Ildar Kamalov
ceabad0fd0 * client: validate form on load 2019-04-23 11:41:53 +03:00
Ildar Kamalov
f76b7c3d94 * client: installation wizard additional checks 2019-04-23 11:41:53 +03:00
Simon Zolin
9e68a522cb * update openapi.yaml 2019-04-23 11:41:53 +03:00
Simon Zolin
faa7c7b2d4 + service install: a post-install guide of what to do next 2019-04-23 11:41:53 +03:00
Simon Zolin
d326d1bc8b * control: /install/configure: validate port number 2019-04-23 11:41:53 +03:00
Simon Zolin
73fbe8b95a * control: /install/configure: reset configuration back to its current state on error 2019-04-23 11:41:53 +03:00
Simon Zolin
87147ac89f + control: /install/*: test TCP port availability for DNS server 2019-04-23 11:41:53 +03:00
Simon Zolin
8d936b5756 + control: /install/check_config: Check and deactivate DNSStubListener 2019-04-23 11:41:53 +03:00
Simon Zolin
4ca24b7707 * control: /install/configure: refactor 2019-04-23 11:41:53 +03:00
Simon Zolin
133dd75ec3 + control: add /install/check_config handler 2019-04-23 11:41:53 +03:00
Simon Zolin
00c128f0a4 * control: /install/get_addresses: don't check if ports are available
* always use port 80
2019-04-23 11:41:53 +03:00
Simon Zolin
e4b53db558 + app: unix, windows: require root user on first launch 2019-04-23 11:41:53 +03:00
Simon Zolin
1611057852 * setRlimit(): move OS-specific code to separate files 2019-04-22 17:41:10 +03:00
Simon Zolin
00ba63341b Merge: Don't use the system default resolver implicitly
Close #647

* commit 'a1b18776678ee894cd0b558cf8683845056a2dfa':
  + parental, safesearch: use our own DNS resolver instead of system default
  + control: use our own DNS resolver instead of system default
2019-04-22 11:28:43 +03:00
Simon Zolin
a1b1877667 + parental, safesearch: use our own DNS resolver instead of system default 2019-04-18 14:31:13 +03:00
Simon Zolin
bebdc1b5bc + control: use our own DNS resolver instead of system default 2019-04-18 12:47:22 +03:00
Ildar Kamalov
1836e56e6e Merge pull request in DNS/adguard-dns from feature/393 to master
* commit 'f83d026c330ec56b76dc995afabe67bc45f654c7':
  + client: privacy policy link
2019-04-17 18:59:05 +03:00
Ildar Kamalov
f83d026c33 + client: privacy policy link
Closes #393
2019-04-17 15:03:25 +03:00
Alexey Dmitrievskiy
61554ba4e0 Merge: * app, dnsforward: add MinVersion for TLS configs #651
* commit 'c82887d3aa3332cf4d7a7dda22f51b160be306b3':
  * app, dnsforward: add MinVersion for TLS configs
2019-04-17 13:56:20 +03:00
Aleksey Dmitrevskiy
c82887d3aa * app, dnsforward: add MinVersion for TLS configs 2019-04-17 12:02:56 +03:00
Alexey Dmitrievskiy
faeda3f075 Merge: * safesearch: Enfore safe search for duckduckgo #268
* commit 'dfa39293a189f59dfd5da335aa66d80b04568a10':
  Update safesearch.go
  Update safesearch.go
  Update safesearch.go
2019-04-16 18:44:51 +03:00
Alexey Dmitrievskiy
afeadbb454 Merge: increase the default timeout used for downloading filters updates #643
* commit 'c78cee339628c8f91a1c698e731c25d99dec8c8b':
  * control: increase http.Client timeout to 5 minutes
  * control: increase the default timeout used for downloading filters updates
2019-04-16 18:40:44 +03:00
Ildar Kamalov
0e031a4921 Merge pull request in DNS/adguard-dns from fix/710 to master
* commit 'd0942c88c8968b7e410356b1ae89ec7d3e69f2bb':
  * client: replace PUT and DELETE with POST
  * control: replace ensurePUT and ensureDELETE with ensurePOST
2019-04-16 12:19:27 +03:00
Ildar Kamalov
d0942c88c8 * client: replace PUT and DELETE with POST 2019-04-16 12:11:31 +03:00
Ildar Kamalov
794d302ce5 * control: replace ensurePUT and ensureDELETE with ensurePOST 2019-04-16 12:11:12 +03:00
Simon Zolin
7f1f85b08c Merge: + http server: enable gzip compression
Close #626

* commit '3f404bc37e14bbd781f6feb62502a44188b4c7ef':
  + http server: enable gzip compression for /control/querylog
  + http server: enable gzip compression for static documents
2019-04-16 11:11:01 +03:00
Simon Zolin
c8e4f61534 Merge: + config: add "rlimit_nofile" setting - Maximum number of opened fd's per process
* commit '1f1e26f67b00ee4983c65e4e3aabf59a488f687b':
  + config: add "rlimit_nofile" setting - Maximum number of opened fd's per process
2019-04-15 18:34:53 +03:00
Simon Zolin
3f404bc37e + http server: enable gzip compression for /control/querylog 2019-04-15 16:21:12 +03:00
Simon Zolin
7911a24dc8 Merge: app: add '--pidfile FILE' command-line parameter
Close #609

* commit '7746a3e6a9f204e7b7ab498c2f484cf426fb1a18':
  + app: add '--pidfile FILE' command-line parameter
2019-04-15 15:36:22 +03:00
Simon Zolin
7746a3e6a9 + app: add '--pidfile FILE' command-line parameter 2019-04-15 13:41:46 +03:00
Simon Zolin
08bedacf0a Merge: DHCP: check/set static IP
Close #686

* commit '828d3121be807daa8f839dfa7a7ac4ba8a6e7cd8':
  * client: show message if there is no static ip
  * client: rename constant
  * hasStaticIP: use properly named boolean variable
  + client: static_ip warnings
  * client: error text
  * client: disable DHCP check if server enabled and hide errors on disable
  * client: hide error if DHCP enabled and require check DHCP before enabling
  * client: accordion styles
  - client: fix DHCP fields validation
  * client: fix DHCP error message
  + config: set default parameters for DHCP server
  + /control/dhcp/set_config: set static IP
  + /control/dhcp/find_active_dhcp: detect static IP on Linux
  * /control/dhcp/find_active_dhcp: new JSON response format
2019-04-15 12:59:16 +03:00
Ildar Kamalov
74a0938038 Merge pull request in DNS/adguard-dns from fix/700 to master
* commit '4d217583b099da6a2a2773d0870ab39e36a9b141':
  - client: validate encryption if enabled
2019-04-11 09:56:49 +03:00
Ildar Kamalov
828d3121be * client: show message if there is no static ip 2019-04-05 15:15:56 +03:00
Ildar Kamalov
b6ae539c36 * client: rename constant 2019-04-05 14:32:56 +03:00
Simon Zolin
fa5ff053b7 * hasStaticIP: use properly named boolean variable 2019-04-05 12:47:26 +03:00
Ildar Kamalov
6bf57ae84e + client: static_ip warnings 2019-04-04 17:13:51 +03:00
Ildar Kamalov
472dc0b77d * client: error text 2019-04-04 17:13:51 +03:00
Ildar Kamalov
4b821d67f5 * client: disable DHCP check if server enabled and hide errors on disable 2019-04-04 17:13:51 +03:00
Ildar Kamalov
24fc2957c5 * client: hide error if DHCP enabled and require check DHCP before enabling 2019-04-04 17:13:51 +03:00
Ildar Kamalov
6ba0e4686a * client: accordion styles 2019-04-04 17:13:51 +03:00
Ildar Kamalov
b92fb34f37 - client: fix DHCP fields validation 2019-04-04 17:13:51 +03:00
Ildar Kamalov
ffd9f1aaa9 * client: fix DHCP error message 2019-04-04 17:13:51 +03:00
Simon Zolin
d43290fe31 + config: set default parameters for DHCP server 2019-04-04 17:13:51 +03:00
Simon Zolin
ef22a31a93 + /control/dhcp/set_config: set static IP 2019-04-04 17:13:51 +03:00
Simon Zolin
0ed619c9c8 + /control/dhcp/find_active_dhcp: detect static IP on Linux 2019-04-04 17:13:51 +03:00
Simon Zolin
8b6e9ef5f9 * /control/dhcp/find_active_dhcp: new JSON response format 2019-04-04 17:13:51 +03:00
Simon Zolin
cca61a33c6 + http server: enable gzip compression for static documents 2019-04-04 16:58:31 +03:00
Ildar Kamalov
4d217583b0 - client: validate encryption if enabled
Closes #700
2019-04-04 14:54:57 +03:00
Aleksey Dmitrevskiy
c78cee3396 * control: increase http.Client timeout to 5 minutes 2019-04-04 11:04:14 +03:00
Ildar Kamalov
b453d9f41d Merge pull request in DNS/adguard-dns from fix/688 to master
* commit 'e231230f1b2045b705602b2c9d3a14c47f276273':
  - client: fixed leases table on mobile
2019-04-03 12:35:06 +03:00
Ildar Kamalov
e231230f1b - client: fixed leases table on mobile
Closes #688
2019-04-02 13:59:03 +03:00
Aleksey Dmitrevskiy
850e856e6e * control: increase the default timeout used for downloading filters updates 2019-04-01 11:30:26 +03:00
Ildar Kamalov
d6b83d4a63 Merge pull request #187 in DNS/adguard-dns from fix/655 to master
* commit '0c973334be33c77e509676d65394d99ead4b26e2':
  * client: if 0.0.0.0 is selected, then redirect to the current IP
2019-03-29 17:38:02 +03:00
Ildar Kamalov
0c973334be * client: if 0.0.0.0 is selected, then redirect to the current IP
Closes #655
2019-03-29 16:24:59 +03:00
Ildar Kamalov
23ac1726b7 Merge pull request #184 in DNS/adguard-dns from fix/660 to master
* commit '91af0cddcea531dfb7972f56371edf17ef763065':
  - client: header links overflow
  - client: fix settings page if current interface doesn't exist
  - client: request version after DNS status request
  - client: fixed Chrome translation popup issue with Portuguese
  - client: missed translation
2019-03-28 13:59:56 +03:00
Simon Zolin
1f1e26f67b + config: add "rlimit_nofile" setting - Maximum number of opened fd's per process 2019-03-28 13:56:12 +03:00
Ildar Kamalov
91af0cddce - client: header links overflow 2019-03-28 12:28:57 +03:00
Ildar Kamalov
6f014fa53d - client: fix settings page if current interface doesn't exist
Closes #664
2019-03-28 11:53:39 +03:00
Ildar Kamalov
6f2e852e09 - client: request version after DNS status request
Closes #660
2019-03-28 11:45:31 +03:00
Ildar Kamalov
fd82e7c26a - client: fixed Chrome translation popup issue with Portuguese
Closes #656
2019-03-28 11:44:10 +03:00
Ildar Kamalov
2bb5b24d4e - client: missed translation
Closes #652
2019-03-28 11:43:26 +03:00
Simon Zolin
92e70515ae Merge: filter: name was reset after an update
close #661

* commit '3367b9fb2a5e541766fa00a68b72121ac00efcff':
  - filter: name was reset after an update
2019-03-27 14:14:37 +03:00
Simon Zolin
51aec7cbbc Merge: Bump version to v0.94
* commit '395ddb7b3caf97c4c281badf0882dc3baf7f3b88':
  Bump version to v0.94
2019-03-27 11:36:21 +03:00
Simon Zolin
3367b9fb2a - filter: name was reset after an update
After filter is updated its name is reset to the value of Title from
 the filter's contents, which may be ""
2019-03-26 19:04:50 +03:00
Johann Richard
dfa39293a1 Update safesearch.go
* Beautify again with`gofmt -s`
2019-03-24 19:34:29 +01:00
Johann Richard
65364930f7 Update safesearch.go
* Beautify the addition
2019-03-24 19:26:40 +01:00
Johann Richard
53ea2d28cf Update safesearch.go
* Add SafeSearch for duckduckgo.com
2019-03-24 19:24:31 +01:00
Andrey Meshkov
395ddb7b3c Bump version to v0.94 2019-03-22 16:57:40 +03:00
Andrey Meshkov
1448b7ab16 Merge pull request 156
Closes #593
Closes #589

* commit '6edfe1bb8e5db8a38104b69bb436736195fa6980':
  Add GOMIPS=softfloat
  More platforms
2019-03-22 16:48:36 +03:00
Andrey Meshkov
abd58004b8 Closes #649 2019-03-22 14:43:43 +03:00
Andrey Meshkov
800cb177f3 Merge pull request #179 in DNS/adguard-dns from feature/637 to master
* commit 'bc0b0af06b71197d75c8a7f8c3a401d4c8a49d61':
  + client: added name for client
  - openapi: fix typo
  + control: /clients: get the list of clients' IP addresses and names from /etc/hosts
2019-03-22 14:27:15 +03:00
Andrey Meshkov
31a0dde515 fix link 2019-03-22 13:57:58 +03:00
Andrey Meshkov
6edfe1bb8e Add GOMIPS=softfloat 2019-03-22 10:40:38 +03:00
Andrey Meshkov
7ebfaccc6e Merge with master 2019-03-22 10:36:48 +03:00
Ildar Kamalov
bc0b0af06b + client: added name for client 2019-03-20 18:37:00 +03:00
Simon Zolin
5210d214ec - openapi: fix typo 2019-03-20 18:37:00 +03:00
Simon Zolin
ed942f3e31 + control: /clients: get the list of clients' IP addresses and names from /etc/hosts 2019-03-20 18:37:00 +03:00
Alexey Dmitrievskiy
5b417d9f17 Merge: add ability to set DNS upstream per domain #445
* commit 'f7860c893da81bc23220bcdec8161ce7b36dc558':
  * client: removed links from upstream DNS translations
  * control, client: fix issues from review
  + control, dns, client: add ability to set DNS upstream per domain
2019-03-20 18:15:31 +03:00
Ildar Kamalov
f7860c893d * client: removed links from upstream DNS translations 2019-03-20 15:23:36 +03:00
Aleksey Dmitrevskiy
a01ba5dd4d * control, client: fix issues from review 2019-03-20 15:19:34 +03:00
Simon Zolin
b6d0d94990 Merge: * DHCP: fix and update tests
* commit '9bc5a4570e76873984a4fda21a4d307499b4980a':
  * DHCP: fix and update tests
2019-03-20 14:57:39 +03:00
Aleksey Dmitrevskiy
9ea5c1abe1 + control, dns, client: add ability to set DNS upstream per domain 2019-03-20 14:24:33 +03:00
Simon Zolin
91ec996ffb Merge: Fix update mechanism for filter rules #604 #620
* commit '0647f3fe8666a38cbdc7a747279513338e49568a':
  * filters: rework update mechanism so that UI doesn't get locked while update is in progress
  - filter: update 'LastUpdated' field and 'last-modified' file time  even when filter's content is up to date
  * control: refactor: move filter adding code to a separate function
  * app: refactor: don't rewrite config file after filters are updated
  * filters: refactor: remove unused if-branches
  * control: enable/disable filter: move code to a separate function
  * filters: refactor: don't check Enabled flag inside filter.update() & filter.load()
  - control: filtering/add_url: don't call httpError() twice on error while reconfiguring
  - control: use locks when operating on config.Filters array
  * refactor: move code to loadFilters()
  * filter: use CRC32 to check whether filter data should be updated
  * filter: refactor
2019-03-20 14:22:25 +03:00
Simon Zolin
9bc5a4570e * DHCP: fix and update tests
for:
 + dhcp: handle lease expiration (d68600c5d0)
2019-03-20 14:19:19 +03:00
Simon Zolin
8defb3b39e Merge: control: DHCP: don't return expired leases #584 #567
* commit 'ef789acee4ef990d9f93fa7f718f819b5bdea03d':
  * control: DHCP: don't return expired leases
2019-03-20 12:58:03 +03:00
Simon Zolin
d5e57248a0 Merge: Add "Setup guide" menu item #605
* commit 'c091d10a416b0ea9c72fb1addd95e7194281d9ce':
  * client: update translations
  + client: added setup guide page and DNS addresses popover
  + control: use the list of IP addresses instead of single string in "dns_address"
2019-03-20 12:56:47 +03:00
Simon Zolin
0647f3fe86 * filters: rework update mechanism so that UI doesn't get locked while update is in progress 2019-03-19 17:30:17 +03:00
Simon Zolin
d664a9de1d - filter: update 'LastUpdated' field and 'last-modified' file time
even when filter's content is up to date

* filters: refactor: don't check 'LastUpdated' inside update()
2019-03-19 17:30:17 +03:00
Simon Zolin
b54f540f71 * control: refactor: move filter adding code to a separate function 2019-03-19 17:30:17 +03:00
Simon Zolin
0884116de3 * app: refactor: don't rewrite config file after filters are updated 2019-03-19 17:30:17 +03:00
Simon Zolin
eefdf8449a * filters: refactor: remove unused if-branches
filter.ID == 0:
Useless, because filter ID is assigned either on application load
 or on filter add.

len(filter.Rules) == 0:
Useless, because rules are added either on application load
 or on filter add or on filter enable.
2019-03-19 17:30:17 +03:00
Simon Zolin
ae2c7d00a9 * control: enable/disable filter: move code to a separate function
* don't start updating all filters after 1 filter has been enabled
* unload filter data on disable
2019-03-19 17:30:17 +03:00
Simon Zolin
afa54a1339 * filters: refactor: don't check Enabled flag inside filter.update() & filter.load() 2019-03-19 17:29:07 +03:00
Simon Zolin
56271819ea - control: filtering/add_url: don't call httpError() twice on error while reconfiguring 2019-03-19 17:29:07 +03:00
Simon Zolin
a9b329daf6 - control: use locks when operating on config.Filters array 2019-03-19 17:29:07 +03:00
Simon Zolin
61b1a30aa1 * refactor: move code to loadFilters() 2019-03-19 17:29:07 +03:00
Simon Zolin
b4732c83c5 * filter: use CRC32 to check whether filter data should be updated 2019-03-19 17:29:07 +03:00
Simon Zolin
783ac967a1 * filter: refactor 2019-03-19 17:29:07 +03:00
Ildar Kamalov
c091d10a41 * client: update translations 2019-03-19 16:27:23 +03:00
Ildar Kamalov
756c5ac0d3 + client: added setup guide page and DNS addresses popover
Closes #605
2019-03-19 16:19:53 +03:00
Simon Zolin
ef789acee4 * control: DHCP: don't return expired leases
- fix potential race when lease's data can be modified while UI thread is reading it
2019-03-19 15:54:35 +03:00
Simon Zolin
b1c11a718e Merge: * control: filtering/refresh: force update filters #537
* commit 'd7b1825cf59cb71a3bdb39fe488527b670e3a90c':
  * control: filtering/refresh: force update filters
2019-03-19 15:09:51 +03:00
Simon Zolin
d7b1825cf5 * control: filtering/refresh: force update filters 2019-03-19 14:28:12 +03:00
Simon Zolin
b5eb840d22 + control: use the list of IP addresses instead of single string in "dns_address"
"dns_address":"0.0.0.0" -> "dns_addresses":["127.0.0.1", "::1", ...]
2019-03-19 14:14:58 +03:00
Alexey Dmitrievskiy
6f56eb4c12 Merge pull request #172 in DNS/adguard-dns from fix/525 to master
* commit '6b223e2992f45aaba7ae9d685a741bd76c23d284':
  * dnsfilter: extend logging
  Fix #606, Fix #610 [change] app, config: add symlink support, allow to specify absolute path in log_file
2019-03-18 14:57:24 +03:00
Aleksey Dmitrevskiy
6b223e2992 * dnsfilter: extend logging 2019-03-18 14:50:33 +03:00
Aleksey Dmitrevskiy
43e5d42070 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-dns 2019-03-15 20:15:14 +03:00
Simon Zolin
a58bf0e24e Merge: Fix function comments based on best practices from Effective Go; #638
* commit 'f432eb360952e9bbf95448798ea6a89c4d93dec7':
  Fix function comments based on best practices from Effective Go
2019-03-15 15:06:09 +03:00
CodeLingo Bot
f432eb3609 Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-15 14:59:05 +03:00
Simon Zolin
6e16654344 Merge: DHCP: refactoring, unit-test, readme, on-disk cache, safe restart, expiration handling; #584 #539
* commit 'ef637e1313ce548eab28c9b9f36c6db20a1c62c9':
  + DHCP: step-by-step guide for test setup with Virtual Box
  * control: add logs
  + dhcp: handle lease expiration
  + dhcp: use ICMP for IP conflict detection
  * dhcp: don't allocate a new lease when processing Request message
  * dhcp: don't process Discover/Request packets with empty client HW address
  * dhcp: refactor
  * DHCP: Stop(): wait until the worker is stopped
  * control: safely restart DHCP server
  + DHCP: On-disk database for lease table
  * use golibs v0.1.1: file.SafeWrite()
  + dhcp: test
  * dhcp: remove code which forces an update of current lease's IP in  Request message handler
  * dhcp: refactor;  log client's HW addr
2019-03-15 14:52:18 +03:00
Simon Zolin
ef637e1313 + DHCP: step-by-step guide for test setup with Virtual Box 2019-03-15 14:00:32 +03:00
Simon Zolin
9494b87ca5 * control: add logs 2019-03-15 14:00:32 +03:00
Simon Zolin
d68600c5d0 + dhcp: handle lease expiration 2019-03-15 14:00:32 +03:00
Simon Zolin
8fa2f48136 + dhcp: use ICMP for IP conflict detection
+ 'icmp_timeout_msec' YAML config setting
2019-03-15 14:00:32 +03:00
Simon Zolin
542a67b84e * dhcp: don't allocate a new lease when processing Request message 2019-03-15 14:00:32 +03:00
Simon Zolin
d832d7ce95 * dhcp: don't process Discover/Request packets with empty client HW address 2019-03-15 14:00:32 +03:00
Simon Zolin
92cf7c1aca * dhcp: refactor 2019-03-15 14:00:04 +03:00
Simon Zolin
b5f0d48e7f * DHCP: Stop(): wait until the worker is stopped 2019-03-15 13:56:45 +03:00
Simon Zolin
6f69fb73af * control: safely restart DHCP server
* control: use mutex in all POST,PUT,DELETE handlers
2019-03-15 13:56:45 +03:00
Simon Zolin
67014c40f7 + DHCP: On-disk database for lease table 2019-03-15 13:56:45 +03:00
Simon Zolin
a2e9d69452 * use golibs v0.1.1: file.SafeWrite() 2019-03-15 13:56:45 +03:00
Simon Zolin
e164cff02b + dhcp: test 2019-03-15 13:56:45 +03:00
Simon Zolin
08bf9b0acb * dhcp: remove code which forces an update of current lease's IP in
Request message handler
2019-03-15 13:56:45 +03:00
Simon Zolin
60fa3b2e95 * dhcp: refactor; log client's HW addr 2019-03-15 13:56:45 +03:00
Aleksey Dmitrevskiy
3f93cb3397 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-dns 2019-03-15 11:55:34 +03:00
Alexey Dmitrievskiy
10daf29e2f Merge: [feature] app, config: add symlink support, allow to specify absolute path in log_file; Fix #606, Fix #610
* commit '70c56f7a180e1113c289574e69dd148bc76db511':
  + app, config: add symlink support, allow to specify absolute path in log_file
2019-03-15 11:55:01 +03:00
Aleksey Dmitrevskiy
70c56f7a18 + app, config: add symlink support, allow to specify absolute path in log_file 2019-03-15 11:49:58 +03:00
Aleksey Dmitrevskiy
e9d20651e9 Fix #606, Fix #610 [change] app, config: add symlink support, allow to specify absolute path in log_file 2019-03-14 18:06:53 +03:00
Ildar Kamalov
466807638c Merge pull request #168 in DNS/adguard-dns from fix/625 to master
* commit 'b1f707d18cf6ce63fe1ed56efbd18d4f614d0543':
  - client: load favicon from local server
2019-03-14 11:30:21 +03:00
Alexey Dmitrievskiy
991717e150 Merge pull request #166 in DNS/adguard-dns from fix/598 to master
* commit 'd06cc0f8ee89a0142c493b33d295272ba100aaf5':
  Fix #598 - [change] service: windows: register sercive to work under local system user
2019-03-13 11:22:28 +03:00
Ildar Kamalov
b1f707d18c - client: load favicon from local server
Closes #625
2019-03-12 18:59:58 +03:00
Alexey Dmitrievskiy
f857ed74ec Merge pull request #154 in DNS/adguard-dns from fix/596 to master
* commit 'b74eded414cf3f31da6feb185619afab85233b5a': (34 commits)
  [change] control: fix issues from review
  [change] config: fix default upstreams list
  * client: typo
  * client: remove log
  * client: fix grammar
  [change] control: add upstreams validation before dns config test
  [change] control: add upstreams validation
  [change] control: update bootstrap DNS check
  * client: remove empty elements from upstream and bootstrap
  * client: locales and pass object to testUpstream
  [change] config, control, openapi: fix issues from reviw
  [fix] control: fix json decode for upstream config
  * client: upstream form
  [change] control, openapi: Handle upstreams test with JSON
  [change] upgrade_test: rework tests
  [change] upgrade_test: add test for upgrade
  [change] control: Remove unuseful check
  [change] control: Fix issues from review
  [change] dnsforward: Add comments for public fields
  [change] control: Handle upstream config with JSON
  ...
2019-03-12 14:30:31 +03:00
Ildar Kamalov
2f497cf5d0 Merge pull request #167 in DNS/adguard-dns from feature/611 to master
* commit '21db166be048bb9d99455838f2ce0a24e85162cd':
  + client: added Simplified Chinese
2019-03-12 12:30:03 +03:00
Ildar Kamalov
21db166be0 + client: added Simplified Chinese
Closes #611
2019-03-12 12:04:41 +03:00
Aleksey Dmitrevskiy
d06cc0f8ee Fix #598 - [change] service: windows: register sercive to work under local system user 2019-03-11 19:18:18 +03:00
Ildar Kamalov
b74eded414 Merge branch 'master' into fix/596 2019-03-11 11:07:28 +03:00
Ildar Kamalov
ecf0b0c5c1 Merge pull request #163 in DNS/adguard-dns from feature/615 to master
* commit '10fcfefcfd7220d68880cd735a3844ec4e617555':
  * client: bulgarian language translation
2019-03-11 11:03:02 +03:00
Alexey Dmitrievskiy
64c2f2d89c Merge pull request #164 in DNS/adguard-dns from fix/621 to master
* commit '0b96bd17c47a87bce8eba2913fdd4b6e0e68d290':
  Fix #621 - [change] config: change the default MalwareDomainList URL protocol
2019-03-11 10:56:24 +03:00
Aleksey Dmitrevskiy
0b96bd17c4 Fix #621 - [change] config: change the default MalwareDomainList URL protocol 2019-03-07 19:12:41 +03:00
Aleksey Dmitrevskiy
392c16cd27 [change] control: fix issues from review 2019-03-07 16:32:52 +03:00
Ildar Kamalov
10fcfefcfd * client: bulgarian language translation 2019-03-07 12:38:17 +03:00
Aleksey Dmitrevskiy
2bd9923691 Merge branch 'fix/596' of ssh://bit.adguard.com:7999/dns/adguard-dns into fix/596 2019-03-07 12:04:58 +03:00
Aleksey Dmitrevskiy
c8c663f3f0 [change] config: fix default upstreams list 2019-03-07 12:04:22 +03:00
Ildar Kamalov
bf82ccede9 * client: typo 2019-03-07 12:02:49 +03:00
Ildar Kamalov
982f8dc1e1 * client: remove log 2019-03-06 18:50:17 +03:00
Ildar Kamalov
83877fb871 * client: fix grammar 2019-03-06 18:39:14 +03:00
Aleksey Dmitrevskiy
ac131923a2 [change] control: add upstreams validation before dns config test 2019-03-06 18:36:31 +03:00
Aleksey Dmitrevskiy
bc4c2e2ff7 Merge branch 'master' into fix/596 2019-03-06 18:25:42 +03:00
Aleksey Dmitrevskiy
1b15bee2b0 [change] control: add upstreams validation 2019-03-06 18:24:14 +03:00
Aleksey Dmitrevskiy
8e09424774 Merge branch 'fix/596' of ssh://bit.adguard.com:7999/dns/adguard-dns into fix/596 2019-03-06 18:06:33 +03:00
Aleksey Dmitrevskiy
89b6323f03 [change] control: update bootstrap DNS check 2019-03-06 18:06:26 +03:00
Ildar Kamalov
cdc568d8d9 * client: remove empty elements from upstream and bootstrap 2019-03-06 17:53:04 +03:00
Aleksey Dmitrevskiy
bda6f777c4 Merge branch 'fix/596' of ssh://bit.adguard.com:7999/dns/adguard-dns into fix/596 2019-03-06 17:14:58 +03:00
Ildar Kamalov
bf2781d465 * client: locales and pass object to testUpstream 2019-03-06 16:35:21 +03:00
Aleksey Dmitrevskiy
f2e547a54e [change] config, control, openapi: fix issues from reviw 2019-03-06 16:17:15 +03:00
Aleksey Dmitrevskiy
ceaa1e4ebf [fix] control: fix json decode for upstream config 2019-03-06 15:35:22 +03:00
Ildar Kamalov
5d6c980ac7 * client: upstream form 2019-03-06 14:45:21 +03:00
Aleksey Dmitrevskiy
e973c4b174 [change] control, openapi: Handle upstreams test with JSON 2019-03-05 12:29:52 +03:00
Aleksey Dmitrevskiy
d9d641941c [change] upgrade_test: rework tests 2019-03-01 11:27:15 +03:00
Aleksey Dmitrevskiy
d318545913 [change] upgrade_test: add test for upgrade 2019-03-01 09:33:00 +03:00
Alexey Dmitrievskiy
7ec4aa91ea Merge pull request #153 in DNS/adguard-dns from fix/595 to master
* commit '2dc2a0946a28f41736228db1065654716907c5b8':
  Fix #595 - Start using GO 1.12
2019-02-28 18:05:27 +03:00
Alexey Dmitrievskiy
681fb4e705 Merge pull request #160 in DNS/adguard-dns from fix/597 to master
* commit '53d680a5dfb027831bdbfc91bde5272d4d849a1a':
  Fix #597 - [bugfix] querylog_top: Empty domain gets to the Top Queried domains
2019-02-28 18:05:19 +03:00
Simon Zolin
c443672e35 Merge pull request #157 in DNS/adguard-dns from feature/600 to master
* commit '6b7a8078b0155ba81678e71fe53f447f444dce8b':
  * control: use single line comment
  * disable config saving if invalid pair
  * control: TLS: don't return empty error messages
  * control: move TLS handlers to a separate file
  * helper functions return 'error', not 'int'
  * tlsConfigStatus.usable is public, renamed ("ValidPair") and is exported to json ("valid_pair")
  * validateCertificates(): split the function's code
  * validateCertificates(): change input parameters;  added short description
  + add tests for validateCertificates()
2019-02-28 18:04:37 +03:00
Aleksey Dmitrevskiy
53d680a5df Fix #597 - [bugfix] querylog_top: Empty domain gets to the Top Queried domains 2019-02-28 16:19:23 +03:00
Simon Zolin
6b7a8078b0 * control: use single line comment 2019-02-28 15:28:49 +03:00
Aleksey Dmitrevskiy
91f8ab0549 [change] control: Remove unuseful check 2019-02-28 15:18:51 +03:00
Aleksey Dmitrevskiy
a8812908c1 [change] control: Fix issues from review 2019-02-28 15:06:30 +03:00
Ildar Kamalov
b8595b87c4 * disable config saving if invalid pair 2019-02-28 14:59:25 +03:00
Aleksey Dmitrevskiy
acb4a98466 [change] dnsforward: Add comments for public fields 2019-02-28 13:40:40 +03:00
Aleksey Dmitrevskiy
3929f0da44 [change] control: Handle upstream config with JSON 2019-02-28 13:01:41 +03:00
Simon Zolin
224c2a891d * control: TLS: don't return empty error messages 2019-02-28 11:55:16 +03:00
Aleksey Dmitrevskiy
81e88472cb Merge branch 'fix/542' into fix/596 2019-02-28 11:16:03 +03:00
Aleksey Dmitrevskiy
6b2baba3c7 Add set_upstreams_config function 2019-02-28 11:10:43 +03:00
Aleksey Dmitrevskiy
967a1e6b87 Merge branch 'master' into fix/596 2019-02-27 18:56:36 +03:00
Simon Zolin
241e7ca20c * control: move TLS handlers to a separate file 2019-02-27 18:53:16 +03:00
Aleksey Dmitrevskiy
bc325de13f Add missed logging 2019-02-27 18:49:53 +03:00
Aleksey Dmitrevskiy
ffa4429818 Merge branch 'master' into fix/542 2019-02-27 18:47:01 +03:00
Simon Zolin
24edf7eeb6 * helper functions return 'error', not 'int' 2019-02-27 18:46:04 +03:00
Simon Zolin
99c8cd06c9 Merge remote-tracking branch 'origin/master' into feature/600 2019-02-27 18:40:22 +03:00
Simon Zolin
9a6266588d Merge pull request #158 in DNS/adguard-dns from feature/600-logger to master
* commit 'f21daae02368841bdb7b8bff05bf208e3b9ab136':
  * control: print HTTP request with log.Tracef()
  * use dnsproxy v0.11.2
  * use golibs v0.1.0
  * update go.sum
  * use new logger - AdguardTeam/golibs/log
2019-02-27 18:35:34 +03:00
Simon Zolin
f21daae023 * control: print HTTP request with log.Tracef() 2019-02-27 18:28:52 +03:00
Simon Zolin
7b64f9ff42 * use dnsproxy v0.11.2 2019-02-27 18:28:09 +03:00
Aleksey Dmitrevskiy
1626b6bd5a Fix empty logging 2019-02-27 18:09:57 +03:00
Simon Zolin
eb22d9cdd9 * use golibs v0.1.0 2019-02-27 17:55:23 +03:00
Aleksey Dmitrevskiy
1ed3a9673d Add handles logging 2019-02-27 17:39:07 +03:00
Simon Zolin
5ad9f8ead2 * tlsConfigStatus.usable is public, renamed ("ValidPair") and is exported to json ("valid_pair") 2019-02-27 17:36:02 +03:00
Aleksey Dmitrevskiy
523c5ef10a Refactor httpErrors 2019-02-27 17:28:10 +03:00
Aleksey Dmitrevskiy
a9839e95a0 pointer is unuseful for httpError func 2019-02-27 16:50:19 +03:00
Aleksey Dmitrevskiy
1223965cd4 Code simplify 2019-02-27 16:42:50 +03:00
Aleksey Dmitrevskiy
141b14c94a Remove unuseful library 2019-02-27 16:19:45 +03:00
Aleksey Dmitrevskiy
3a9d436f8a Add schema migration 2019-02-27 16:15:36 +03:00
Simon Zolin
01548c236e * update go.sum 2019-02-27 16:04:05 +03:00
Simon Zolin
5cb6d97cd7 * use new logger - AdguardTeam/golibs/log 2019-02-27 15:02:11 +03:00
Simon Zolin
f4a6ca726c * validateCertificates(): split the function's code 2019-02-27 14:31:53 +03:00
Simon Zolin
766fbab071 * validateCertificates(): change input parameters; added short description 2019-02-27 14:21:15 +03:00
Aleksey Dmitrevskiy
87c8114291 Use gotools 2019-02-27 13:12:06 +03:00
Simon Zolin
d218e047a3 + add tests for validateCertificates() 2019-02-27 13:07:29 +03:00
Aleksey Dmitrevskiy
bf893d488a Refactoring for set upstream and bootstrap DNS 2019-02-27 12:58:42 +03:00
Andrey Meshkov
bd31151c1f More platforms 2019-02-27 12:41:37 +03:00
Simon Zolin
4476262357 Merge pull request #155 in DNS/adguard-dns from carlbennett-carlbennett-patch-1 to master
* commit '2a93e45b671b422bf384548b221360d0c8e22d33':
  Fix typo in client/src/__locales/en.json
2019-02-27 12:06:13 +03:00
Carl Bennett
2a93e45b67 Fix typo in client/src/__locales/en.json 2019-02-27 12:02:06 +03:00
Aleksey Dmitrevskiy
5ba43a59f4 Fix server reconfig 2019-02-27 11:31:25 +03:00
Aleksey Dmitrevskiy
dc05556c5a Fix #542 - Add Bootstrap DNS resolver settings 2019-02-27 11:15:18 +03:00
Aleksey Dmitrevskiy
5bc6d00aa0 Fix #596 - Intelligent Optimal DNS Resolution 2019-02-26 18:19:05 +03:00
Aleksey Dmitrevskiy
2dc2a0946a Fix #595 - Start using GO 1.12 2019-02-26 15:32:56 +03:00
107 changed files with 6429 additions and 4238 deletions

View File

@@ -2,7 +2,7 @@ language: go
sudo: false
go:
- 1.11.x
- 1.12.x
- 1.x
os:
- linux
@@ -38,7 +38,7 @@ matrix:
# Release build configuration
- name: release
go:
- 1.11.x
- 1.12.x
os:
- linux
@@ -66,7 +66,7 @@ matrix:
- name: docker
if: type != pull_request AND (branch = master OR tag IS present)
go:
- 1.11.x
- 1.12.x
os:
- linux
services:

327
AGHTechDoc.md Normal file
View File

@@ -0,0 +1,327 @@
# AdGuard Home Technical Document
The document describes technical details and internal algorithms of AdGuard Home.
Contents:
* First startup
* Installation wizard
* "Get install settings" command
* "Check configuration" command
* Disable DNSStubListener
* "Apply configuration" command
* Enable DHCP server
* "Check DHCP" command
* "Enable DHCP" command
* Static IP check/set
## First startup
The first application startup is detected when there's no .yaml configuration file.
We check if the user is root, otherwise we fail with an error.
Web server is started up on port 3000 and automatically redirects requests to `/` to Installation wizard.
After Installation wizard steps are completed, we write configuration to a file and start normal operation.
## Installation wizard
This is the collection of UI screens that are shown to a user on first application startup.
The screens are:
1. Welcome
2. Set up network interface and listening ports for Web and DNS servers
3. Set up administrator username and password
4. Configuration complete
5. Done
Algorithm:
Screen 2:
* UI asks server for initial information and shows it
* User edits the default settings, clicks on "Next" button
* UI asks server to check new settings
* Server searches for the known issues
* UI shows information about the known issues and the means to fix them
* Server applies automatic fixes of the known issues on command from UI
Screen 3:
* UI asks server to apply the configuration
* Server restarts DNS server
### "Get install settings" command
Request:
GET /control/install/get_addresses
Response:
200 OK
{
"web_port":80,
"dns_port":53,
"interfaces":{
"enp2s0":{"name":"enp2s0","mtu":1500,"hardware_address":"","ip_addresses":["",""],"flags":"up|broadcast|multicast"},
"lo":{"name":"lo","mtu":65536,"hardware_address":"","ip_addresses":["127.0.0.1","::1"],"flags":"up|loopback"},
}
}
If `interfaces.flags` doesn't contain `up` flag, UI must show `(Down)` status next to its IP address in interfaces selector.
### "Check configuration" command
Request:
POST /control/install/check_config
{
"web":{"port":80,"ip":"192.168.11.33"},
"dns":{"port":53,"ip":"127.0.0.1","autofix":false},
}
Server should check whether a port is available only in case it itself isn't already listening on that port.
Server replies on success:
200 OK
{
"web":{"status":""},
"dns":{"status":""},
}
Server replies on error:
200 OK
{
"web":{"status":"ERROR MESSAGE"},
"dns":{"status":"ERROR MESSAGE", "can_autofix": true|false},
}
### Disable DNSStubListener
On Linux, if 53 port is not available, server performs several additional checks to determine if the issue can be fixed automatically.
#### Phase 1
Request:
POST /control/install/check_config
{
"dns":{"port":53,"ip":"127.0.0.1","autofix":false}
}
Check if DNSStubListener is enabled:
systemctl is-enabled systemd-resolved
Check if DNSStubListener is active:
grep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf
If the issue can be fixed automatically, server replies with `"can_autofix":true`
200 OK
{
"dns":{"status":"ERROR MESSAGE", "can_autofix":true},
}
In this case UI shows "Fix" button next to error message.
#### Phase 2
If user clicks on "Fix" button, UI sends request to perform an automatic fix
POST /control/install/check_config
{
"dns":{"port":53,"ip":"127.0.0.1","autofix":true},
}
Deactivate (save backup as `resolved.conf.orig`) and stop DNSStubListener:
sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
systemctl reload-or-restart systemd-resolved
Server replies:
200 OK
{
"dns":{"status":""},
}
### "Apply configuration" command
Request:
POST /control/install/configure
{
"web":{"port":80,"ip":"192.168.11.33"},
"dns":{"port":53,"ip":"127.0.0.1"},
"username":"u",
"password":"p",
}
Server checks the parameters once again, restarts DNS server, replies:
200 OK
On error, server responds with code 400 or 500. In this case UI should show error message and reset to the beginning.
400 Bad Request
ERROR MESSAGE
## Enable DHCP server
Algorithm:
* UI shows DHCP configuration screen with "Enabled DHCP" button disabled, and "Check DHCP" button enabled
* User clicks on "Check DHCP"; UI sends request to server
* Server may fail to detect whether there is another DHCP server working in the network. In this case UI shows a warning.
* Server may detect that a dynamic IP configuration is used for this interface. In this case UI shows a warning.
* UI enables "Enable DHCP" button
* User clicks on "Enable DHCP"; UI sends request to server
* Server sets a static IP (if necessary), enables DHCP server, sends the status back to UI
* UI shows the status
### "Check DHCP" command
Request:
POST /control/dhcp/find_active_dhcp
vboxnet0
Response:
200 OK
{
"other_server": {
"found": "yes|no|error",
"error": "Error message", // set if found=error
},
"static_ip": {
"static": "yes|no|error",
"ip": "<Current dynamic IP address>", // set if static=no
}
}
If `other_server.found` is:
* `no`: everything is fine - there is no other DHCP server
* `yes`: we found another DHCP server. UI shows a warning.
* `error`: we failed to determine whether there's another DHCP server. `other_server.error` contains error details. UI shows a warning.
If `static_ip.static` is:
* `yes`: everything is fine - server uses static IP address.
* `no`: `static_ip.ip` contains the current dynamic IP address which we may set as static. In this case UI shows a warning:
Your system uses dynamic IP address configuration for interface <CURRENT INTERFACE NAME>. In order to use DHCP server a static IP address must be set. Your current IP address is <static_ip.ip>. We will automatically set this IP address as static if you press Enable DHCP button.
* `error`: this means that the server failed to check for a static IP. In this case UI shows a warning:
In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.
### "Enable DHCP" command
Request:
POST /control/dhcp/set_config
{
"enabled":true,
"interface_name":"vboxnet0",
"gateway_ip":"192.169.56.1",
"subnet_mask":"255.255.255.0",
"range_start":"192.169.56.3",
"range_end":"192.169.56.3",
"lease_duration":60,
"icmp_timeout_msec":0
}
Response:
200 OK
OK
### Static IP check/set
Before enabling DHCP server we have to make sure the network interface we use has a static IP configured.
#### Phase 1
On Debian systems DHCP is configured by `/etc/dhcpcd.conf`.
To detect if a static IP is used currently we search for line
interface eth0
and then look for line
static ip_address=...
If the interface already has a static IP, everything is set up, we don't have to change anything.
To get the current IP address along with netmask we execute
ip -oneline -family inet address show eth0
which will print:
2: eth0 inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\ valid_lft forever preferred_lft forever
To get the current gateway address:
ip route show dev enp2s0
which will print:
default via 192.168.0.1 proto dhcp metric 100
#### Phase 2
This method only works on Raspbian.
On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`. This must be configured in `/etc/netplan/01-netcfg.yaml`.
Fedora doesn't use `dhcpcd.conf` configuration at all.
Step 1.
To set a static IP address we add these lines to `dhcpcd.conf`:
interface eth0
static ip_address=192.168.0.1/24
static routers=192.168.0.1
static domain_name_servers=192.168.0.1
* Don't set 'routers' if we couldn't find gateway IP
* Set 'domain_name_servers' equal to our IP
Step 2.
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
ip addr replace dev eth0 192.168.0.1/24

View File

@@ -21,4 +21,4 @@ EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]

View File

@@ -70,7 +70,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
You will need:
* [go](https://golang.org/dl/) v1.11 or later.
* [go](https://golang.org/dl/) v1.12 or later.
* [node.js](https://nodejs.org/en/download/) v10 or later.
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:

262
app.go
View File

@@ -1,23 +1,26 @@
package main
import (
"bufio"
"crypto/tls"
"fmt"
stdlog "log"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/NYTimes/gziphandler"
"github.com/gobuffalo/packr"
"github.com/hmage/golibs/log"
)
// VersionString will be set through ldflags, contains current version
@@ -28,6 +31,7 @@ var httpsServer struct {
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
sync.Mutex // protects config.TLS
}
var pidFileName string // PID file name. Empty if no PID file was created.
const (
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
@@ -45,14 +49,6 @@ func main() {
return
}
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-signalChannel
cleanup()
os.Exit(0)
}()
// run the protection
run(args)
}
@@ -71,14 +67,29 @@ func run(args options) {
// configure log level and output
configureLogger(args)
// enable TLS 1.3
enableTLS13()
// print the first message after logger is configured
log.Printf("AdGuard Home, version %s\n", VersionString)
log.Tracef("Current working directory is %s", config.ourWorkingDir)
log.Debug("Current working directory is %s", config.ourWorkingDir)
if args.runningAsService {
log.Printf("AdGuard Home is running as a service")
log.Info("AdGuard Home is running as a service")
}
config.firstRun = detectFirstRun()
if config.firstRun {
requireAdminRights()
}
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-signalChannel
cleanup()
cleanupAlways()
os.Exit(0)
}()
// Do the upgrade if necessary
err := upgradeConfig()
@@ -92,6 +103,11 @@ func run(args options) {
log.Fatal(err)
}
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
config.RlimitNoFile != 0 {
setRlimit(config.RlimitNoFile)
}
// override bind host/port from the console
if args.bindHost != "" {
config.BindHost = args.bindHost
@@ -100,24 +116,7 @@ func run(args options) {
config.BindPort = args.bindPort
}
// 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] // 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{}
}
}
loadFilters()
// Save the updated config
err = config.write()
@@ -141,27 +140,27 @@ func run(args options) {
}
}
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
pidFileName = args.pidFile
}
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNecessary(false)
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}()
// Schedule automatic filters updates
go periodicallyRefreshFilters()
// Initialize and run the admin Web interface
box := packr.NewBox("build/static")
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
http.Handle("/", postInstallHandler(optionalAuthHandler(http.FileServer(box))))
http.Handle("/", postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(http.FileServer(box)))))
registerControlHandlers()
// add handlers for /install paths, we only need them when we're not configured yet
if config.firstRun {
log.Printf("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
registerInstallHandlers()
}
@@ -169,53 +168,7 @@ func run(args options) {
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
// for https, we have a separate goroutine loop
go func() {
for { // this is an endless loop
httpsServer.cond.L.Lock()
// this mechanism doesn't let us through until all conditions are ment
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
httpsServer.cond.Wait()
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
// validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(config.TLS)
if !data.usable {
log.Fatal(data.WarningValidation)
os.Exit(1)
}
config.Lock()
config.TLS = data // update warnings
config.Unlock()
// prepare certs for HTTPS server
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
certchain := make([]byte, len(config.TLS.CertificateChain))
copy(certchain, []byte(config.TLS.CertificateChain))
privatekey := make([]byte, len(config.TLS.PrivateKey))
copy(privatekey, []byte(config.TLS.PrivateKey))
cert, err := tls.X509KeyPair(certchain, privatekey)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
httpsServer.cond.L.Unlock()
// prepare HTTPS server
httpsServer.server = &http.Server{
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
printHTTPAddresses("https")
err = httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed {
log.Fatal(err)
os.Exit(1)
}
}
}()
go httpServerLoop()
// this loop is used as an ability to change listening host and/or port
for {
@@ -228,13 +181,107 @@ func run(args options) {
}
err := httpServer.ListenAndServe()
if err != http.ErrServerClosed {
cleanupAlways()
log.Fatal(err)
os.Exit(1)
}
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
}
}
func httpServerLoop() {
for {
httpsServer.cond.L.Lock()
// this mechanism doesn't let us through until all conditions are met
for config.TLS.Enabled == false ||
config.TLS.PortHTTPS == 0 ||
config.TLS.PrivateKey == "" ||
config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
httpsServer.cond.Wait()
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
// validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
if !data.ValidPair {
cleanupAlways()
log.Fatal(data.WarningValidation)
}
config.Lock()
config.TLS.tlsConfigStatus = data // update warnings
config.Unlock()
// prepare certs for HTTPS server
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
certchain := make([]byte, len(config.TLS.CertificateChain))
copy(certchain, []byte(config.TLS.CertificateChain))
privatekey := make([]byte, len(config.TLS.PrivateKey))
copy(privatekey, []byte(config.TLS.PrivateKey))
cert, err := tls.X509KeyPair(certchain, privatekey)
if err != nil {
cleanupAlways()
log.Fatal(err)
}
httpsServer.cond.L.Unlock()
// prepare HTTPS server
httpsServer.server = &http.Server{
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
},
}
printHTTPAddresses("https")
err = httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed {
cleanupAlways()
log.Fatal(err)
}
}
}
// Check if the current user has root (administrator) rights
// and if not, ask and try to run as root
func requireAdminRights() {
admin, _ := haveAdminRights()
if admin {
return
}
if runtime.GOOS == "windows" {
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
} else {
log.Error("This is the first launch of AdGuard Home. You must run it as root.")
_, _ = io.WriteString(os.Stdout, "Do you want to start AdGuard Home as root user? [y/n] ")
stdin := bufio.NewReader(os.Stdin)
buf, _ := stdin.ReadString('\n')
buf = strings.TrimSpace(buf)
if buf != "y" {
os.Exit(1)
}
cmd := exec.Command("sudo", os.Args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
os.Exit(1)
}
}
// Write PID to a file
func writePIDFile(fn string) bool {
data := fmt.Sprintf("%d", os.Getpid())
err := ioutil.WriteFile(fn, []byte(data), 0644)
if err != nil {
log.Error("Couldn't write PID to file %s: %v", fn, err)
return false
}
return true
}
// initWorkingDir initializes the ourWorkingDir
// if no command-line arguments specified, we use the directory where our binary file is located
func initWorkingDir(args options) {
@@ -263,7 +310,11 @@ func configureLogger(args options) {
ls.LogFile = args.logFile
}
log.Verbose = ls.Verbose
level := log.INFO
if ls.Verbose {
level = log.DEBUG
}
log.SetLevel(level)
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if nothing else is configured
@@ -283,24 +334,43 @@ func configureLogger(args options) {
}
} else {
logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile)
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
if filepath.IsAbs(ls.LogFile) {
logFilePath = ls.LogFile
}
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("cannot create a log file: %s", err)
}
stdlog.SetOutput(file)
log.SetOutput(file)
}
}
// TODO after GO 1.13 release TLS 1.3 will be enabled by default. Remove this afterward
func enableTLS13() {
err := os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
if err != nil {
log.Fatalf("Failed to enable TLS 1.3: %s", err)
}
}
func cleanup() {
log.Printf("Stopping AdGuard Home")
log.Info("Stopping AdGuard Home")
err := stopDNSServer()
if err != nil {
log.Printf("Couldn't stop DNS server: %s", err)
log.Error("Couldn't stop DNS server: %s", err)
}
err = stopDHCPServer()
if err != nil {
log.Printf("Couldn't stop DHCP server: %s", err)
log.Error("Couldn't stop DHCP server: %s", err)
}
}
// This function is called before application exits
func cleanupAlways() {
if len(pidFileName) != 0 {
os.Remove(pidFileName)
}
}
@@ -312,6 +382,7 @@ type options struct {
bindHost string // host address to bind HTTP server on
bindPort int // port to serve HTTP pages on
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
pidFile string // File name to save PID to
// service control action (see service.ControlAction array + "status" command)
serviceControlAction string
@@ -348,6 +419,7 @@ func loadOptions() options {
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
o.logFile = value
}, nil},
{"pidfile", "", "File name to save PID to", func(value string) { o.pidFile = value }, nil},
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
{"help", "", "print this help", nil, func() {
printHelp()
@@ -373,7 +445,7 @@ func loadOptions() options {
if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
if opt.callbackWithValue != nil {
if i+1 >= len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
log.Error("Got %s without argument\n", v)
os.Exit(64)
}
i++
@@ -386,7 +458,7 @@ func loadOptions() options {
}
}
if !knownParam {
log.Printf("ERROR: unknown option %v\n", v)
log.Error("unknown option %v\n", v)
printHelp()
os.Exit(64)
}

3051
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

3
client/package.json vendored
View File

@@ -47,7 +47,8 @@
"babel-runtime": "6.26.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11",
"css-loader": "^0.28.11",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^2.1.1",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-react-app": "^2.1.0",

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

@@ -0,0 +1,250 @@
{
"url_added_successfully": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0435\u043d URL",
"check_dhcp_servers": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0437\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440",
"save_config": "\u0417\u0430\u043f\u0438\u0448\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435",
"enabled_dhcp": "DHCP \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d",
"disabled_dhcp": "DHCP \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d",
"dhcp_title": "DHCP \u0441\u044a\u0440\u0432\u044a\u0440 (\u0442\u0435\u0441\u0442\u043e\u0432\u0438!)",
"dhcp_description": "\u0410\u043a\u043e \u0440\u0443\u0442\u0435\u0440\u0430 \u0432\u0438 \u043d\u0435 \u0440\u0430\u0437\u0434\u0430\u0432\u0430 DHCP \u0430\u0434\u0440\u0435\u0441\u0438, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f \u0432 AdGuard DHCP \u0441\u044a\u0440\u0432\u044a\u0440.",
"dhcp_enable": "\u0420\u0437\u0440\u0435\u0448\u0438 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"dhcp_disable": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"dhcp_not_found": "\u0412\u0430\u0448\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u043d\u044f\u043c\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440. \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0435 \u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f DHCP \u0441\u044a\u0440\u0432\u044a\u0440.",
"dhcp_found": "\u0412\u0430\u0448\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u0432\u0435\u0447\u0435 \u0438\u043c\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440. \u041d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0442\u043e\u0440\u0438!",
"dhcp_leases": "DHCP \u0440\u0430\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_leases_not_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u0438 DHCP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_config_saved": "\u0417\u0430\u043f\u0438\u0448\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"form_error_required": "\u0417\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043f\u043e\u043b\u0435",
"form_error_ip_format": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IPv4 \u0430\u0434\u0440\u0435\u0441",
"form_error_positive": "\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u0447\u0438\u0441\u043b\u043e",
"dhcp_form_gateway_input": "IP \u0448\u043b\u044e\u0437",
"dhcp_form_subnet_input": "\u041c\u0440\u0435\u0436\u043e\u0432\u0430 \u043c\u0430\u0441\u043a\u0430",
"dhcp_form_range_title": "\u0413\u0440\u0443\u043f\u0430 \u043e\u0442 IP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_form_range_start": "\u041f\u044a\u0440\u0432\u0438 \u0430\u0434\u0440\u0435\u0441",
"dhcp_form_range_end": "\u041f\u043e\u0441\u043b\u0435\u0434\u0435\u043d \u0430\u0434\u0440\u0435\u0441",
"dhcp_form_lease_title": "\u041e\u0442\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)",
"dhcp_form_lease_input": "\u041e\u0442\u0447\u0435\u0442 \u0437\u0430 \u0440\u0430\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_interface_select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0440\u0435\u0436\u043e\u0432 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0437\u0430 DHCP",
"dhcp_hardware_address": "\u0425\u0430\u0440\u0434\u0443\u0435\u0440\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 (MAC)",
"dhcp_ip_addresses": "IP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_table_hostname": "\u0418\u043c\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e",
"dhcp_table_expires": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f",
"dhcp_warning": "\u0410\u043a\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f DHCP \u0441\u044a\u0440\u0432\u044a\u0440, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u044f\u043c\u0430 \u0434\u0440\u0443\u0433 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u0412\u0438!",
"back": "\u041d\u0430\u0437\u0430\u0434",
"dashboard": "\u0422\u0430\u0431\u043b\u043e",
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"filters": "\u0424\u0438\u043b\u0442\u0440\u0438",
"query_log": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043d\u0430 \u0437\u0430\u044f\u0432\u043a\u0438\u0442\u0435",
"faq": "\u0427\u0417\u0412",
"version": "\u0432\u0435\u0440\u0441\u0438\u044f",
"address": "\u0430\u0434\u0440\u0435\u0441",
"on": "\u0412\u041a\u041b\u042e\u0427\u0415\u041d\u041e",
"off": "\u0418\u0417\u041a\u041b\u042e\u0427\u0415\u041d\u041e",
"copyright": "\u0410\u0432\u0442\u043e\u0440\u0441\u043a\u043e \u043f\u0440\u0430\u0432\u043e",
"homepage": "\u0414\u043e\u043c\u0430\u0448\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
"report_an_issue": "\u0421\u044a\u043e\u0431\u0449\u0438 \u0437\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c",
"enable_protection": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0437\u0430\u0449\u0438\u0442\u0430",
"enabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430\u0442\u0430 \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430",
"disable_protection": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0437\u0430\u0449\u0438\u0442\u0430",
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430\u0442\u0430 \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d\u0430",
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430\u0442\u0430",
"dns_query": "DNS \u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f",
"blocked_by": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u043e\u0442",
"stats_malware_phishing": "\u0432\u0438\u0440\u0443\u0441\u0438\/\u0430\u0442\u0430\u043a\u0438",
"stats_adult": "\u0441\u0430\u0439\u0442\u043e\u0432\u0435 \u0437\u0430 \u0432\u044a\u0437\u0440\u0430\u0441\u0442\u043d\u0438",
"stats_query_domain": "\u041d\u0430\u0439-\u043e\u0442\u0432\u0430\u0440\u044f\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0438",
"for_last_24_hours": "\u0437\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 24 \u0447\u0430\u0441\u0430",
"no_domains_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0440\u0435\u0437\u0443\u043b\u0442\u0430\u0442\u0438",
"requests_count": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0437\u0430\u044f\u0432\u043a\u0438\u0442\u0435",
"top_blocked_domains": "\u041d\u0430\u0439-\u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0438",
"top_clients": "\u041d\u0430\u0439-\u0430\u043a\u0442\u0438\u0432\u043d\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0438",
"no_clients_found": "\u041d\u044f\u043ca \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"general_statistics": "\u041e\u0431\u0449\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0438\u043a\u0430",
"number_of_dns_query_24_hours": "\u0421\u0443\u043c\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u0437\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 24 \u0447\u0430\u0441\u0430",
"number_of_dns_query_blocked_24_hours": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043e\u0442 \u0444\u0438\u043b\u0442\u0440\u0438\u0442\u0435 \u0437\u0430 \u0440\u0435\u043a\u043b\u0430\u043c\u0430 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438",
"number_of_dns_query_blocked_24_hours_by_sec": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043e\u0442 AdGuard \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0441 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442\u0442\u0430",
"number_of_dns_query_blocked_24_hours_adult": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0441\u0430\u0439\u0442\u043e\u0432\u0435 \u0437\u0430 \u0432\u044a\u0437\u0440\u0430\u0441\u0442\u043d\u0438",
"enforced_save_search": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"number_of_dns_query_to_safe_search": "\u0421\u0443\u043c\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043f\u0440\u0438 \u043a\u043e\u0439\u0442\u043e \u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u043e \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"average_processing_time": "\u0421\u0440\u0435\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430",
"average_processing_time_hint": "\u0421\u0440\u0435\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u0432 \u043c\u0438\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0438",
"block_domain_use_filters_and_hosts": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0434\u043e\u043c\u0435\u0439\u043d\u0438 - \u043e\u0431\u0449\u0438 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"filters_block_toggle_hint": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 <a href='#filters'>\u0424\u0438\u043b\u0442\u0440\u0438<\/a>.",
"use_adguard_browsing_sec": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 AdGuard \u043c\u043e\u0434\u0443\u043b \u0437\u0430 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442\u0442\u0430",
"use_adguard_browsing_sec_hint": "\u041c\u043e\u0434\u0443\u043b \u0421\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442 \u0432 AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0432\u0430 \u0432\u0441\u044f\u043a\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043a\u043e\u044f\u0442\u043e \u043e\u0442\u0432\u0430\u0440\u044f\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u0432 \u0447\u0435\u0440\u043d\u0438\u0442\u0435 \u0441\u043f\u0438\u0441\u044a\u0446\u0438 \u0437\u0430\u0441\u0442\u0440\u0430\u0448\u0430\u0432\u0430\u0449\u0438 \u0432\u0430\u0448\u0430\u0442\u0430 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0435\u043d \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0439\u0442\u043e \u0437\u0430\u0449\u0438\u0442\u0430\u0432\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u043e\u0441\u0442 \u0438 \u0438\u0437\u043f\u0440\u0430\u0449\u0430 \u0441\u0430\u043c\u043e SHA256 \u0441\u0443\u043c\u0430 \u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0430 \u043d\u0430 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0434\u043e\u043c\u0435\u0439\u043d\u0430 \u043a\u043e\u0439\u0442\u043e \u043f\u043e\u0441\u0435\u0449\u0430\u0432\u0430\u0442\u0435.",
"use_adguard_parental": "\u0412\u043a\u043b\u044e\u0447\u0438 AdGuard \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"use_adguard_parental_hint": "\u041c\u043e\u0434\u0443\u043b XXX \u0432 AdGuard Home \u0449\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438 \u0434\u0430\u043b\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0438\u043c\u0430 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u0438 \u0437\u0430 \u0432\u044a\u0437\u0432\u044a\u0441\u0442\u043d\u0438. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0435 \u0441\u044a\u0449\u0438\u044f API \u0437\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u043e\u0441\u0442 \u043a\u0430\u0442\u043e \u043f\u0440\u0438 \u043c\u043e\u0434\u0443\u043b\u0430 \u0437\u0430 \u0421\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442.",
"enforce_safe_search": "\u0412\u043a\u043b\u044e\u0447\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enforce_save_search_hint": "AdGuard Home \u043f\u0440\u0438\u043b\u0430\u0433\u0430 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435 \u0432 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0442\u044a\u0440\u0441\u0430\u0447\u043a\u0438 \u0438 \u0441\u0430\u0439\u0442\u043e\u0432\u0435: Google, Youtube, Bing, \u0438 Yandex.",
"no_servers_specified": "\u041d\u044f\u043c\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438 \u0443\u0441\u043b\u0443\u0433\u0438",
"no_settings": "\u041d\u044f\u043c\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"general_settings": "\u041e\u0431\u0449\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"upstream_dns": "\u0413\u043b\u0430\u0432\u0435\u043d DNS \u0441\u044a\u0440\u0432\u044a\u0440",
"upstream_dns_hint": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, AdGuard Home \u0449\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0437\u0430 \u0433\u043b\u0430\u0432\u0435\u043d. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439 tls:\/\/ \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043a\u0430 \u0437\u0430 DNS \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0449\u0438 TLS \u0432\u0440\u044a\u0437\u043a\u0430.",
"test_upstream_btn": "\u0422\u0435\u0441\u0442\u0432\u0430\u0439 \u0433\u043b\u0430\u0432\u043d\u0438\u044f DNS",
"apply_btn": "\u041f\u0440\u0438\u043b\u043e\u0436\u0438",
"disabled_filtering_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e",
"enabled_filtering_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0444\u0438\u0442\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e",
"disabled_safe_browsing_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e-\u0441\u044a\u0440\u0444\u0438\u0440\u0430\u043d\u0435",
"enabled_safe_browsing_toast": "\u0420\u0437\u0440\u0435\u0448\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e-\u0441\u044a\u0440\u0444\u0438\u0440\u0430\u043d\u0435",
"disabled_parental_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"enabled_parental_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"disabled_safe_search_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enabled_save_search_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enabled_table_header": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438",
"name_table_header": "\u0418\u043c\u0435",
"filter_url_table_header": "URL \u0444\u0438\u043b\u0442\u044a\u0440",
"rules_count_table_header": "\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u0431\u0449\u043e",
"last_time_updated_table_header": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0435\u043d",
"actions_table_header": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f",
"delete_table_action": "\u0418\u0437\u0442\u0440\u0438\u0439",
"filters_and_hosts": "\u0427\u0435\u0440\u043d\u0438 \u0441\u043f\u0438\u0441\u044a\u0446\u0438 \u0441 \u043e\u0431\u0449\u0438 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"filters_and_hosts_hint": "AdGuard Home \u0440\u0430\u0437\u0431\u0438\u0440\u0430 adblock \u0438 host \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441.",
"no_filters_added": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u0435\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"add_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u0438 \u0444\u0438\u043b\u0442\u044a\u0440",
"cancel_btn": "\u041e\u0442\u043a\u0430\u0436\u0438",
"enter_name_hint": "\u0412\u044a\u0432\u0435\u0434\u0438 \u0438\u043c\u0435",
"enter_url_hint": "\u0412\u044a\u0432\u0435\u0434\u0438 URL",
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438 \u0437\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f",
"new_filter_btn": "\u0412\u044a\u0432\u0435\u0434\u0438 \u043d\u043e\u0432 \u0444\u0438\u043b\u0442\u044a\u0440",
"enter_valid_filter_url": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d URL \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0438\u043b\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 host \u043f\u0440\u0430\u0432\u043e\u043f\u0438\u0441\u0430.",
"custom_filter_rules": "\u041c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"custom_filter_rules_hint": "\u0412\u044a\u0432\u0435\u0436\u0434\u0430\u0439\u0442\u0435 \u0432\u0441\u044f\u043a\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043d\u0430 \u043d\u043e\u0432 \u0440\u0435\u0434. \u041c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 adblock \u0438\u043b\u0438 hosts \u0444\u0430\u0439\u043b\u043e\u0432 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441.",
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u0438",
"example_meaning_filter_block": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d example.org \u0438 \u0432\u0441\u0438\u0447\u043a\u0438 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"example_meaning_filter_whitelist": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d example.org \u0438 \u0432\u0441\u0438\u0447\u043a\u0438\u0442\u0435 \u043c\u0443 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"example_meaning_host_block": "AdGuard Home \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 127.0.0.1 = \u043f\u0440\u0430\u0437\u0435\u043d \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0434\u043e\u043c\u0435\u0439\u043d example.org (\u043d\u043e \u043d\u0435 \u0438 \u0437\u0430 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438).",
"example_comment": "! \u0422\u043e\u0432\u0430 \u0435 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_comment_meaning": "\u043f\u0440\u0438\u043c\u0435\u0440 \u0437\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_comment_hash": "# \u0422\u043e\u0432\u0430 \u0435 \u0441\u044a\u0449\u043e \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_regex_meaning": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d\u0438 \u043a\u043e\u0439\u0442\u043e \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442 \u0441\u044a\u0441 \u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e",
"example_upstream_regular": "\u043a\u043b\u0430\u0441\u0438\u0447\u0435\u0441\u043a\u0438 DNS (UDP \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b)",
"example_upstream_dot": "\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d <0>DNS-\u0432\u044a\u0440\u0445\u0443-TLS<\/0>",
"example_upstream_doh": "\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d <0>DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS<\/0>",
"example_upstream_sdns": "\u043c\u043e\u0436\u0435 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 <0>DNS \u041f\u043e\u0434\u043f\u0438\u0441\u0432\u0430\u043d\u0435<\/0> \u0437\u0430 <1>DNSCrypt<\/1> \u0438\u043b\u0438 <2>DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS<\/2> \u0441\u044a\u0440\u0432\u044a\u0440\u0438",
"example_upstream_tcp": "\u043a\u043b\u0430\u0441\u0438\u0447\u0435\u0441\u043a\u0438 DNS (TCP \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0444\u0438\u043b\u0442\u0438 \u0441\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0438",
"updated_upstream_dns_toast": "\u0413\u043b\u043e\u0431\u0430\u043b\u043d\u0438\u0442\u0435 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0441\u0430 \u043e\u0431\u043d\u043e\u0432\u0435\u043d\u0438",
"dns_test_ok_toast": "\u0412\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u0442\u0435 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u044f\u0442 \u043a\u043e\u0440\u0435\u043a\u0442\u043d\u043e",
"dns_test_not_ok_toast": "\u0421\u044a\u0440\u0432\u044a\u0440 \"{{key}}\": \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0438, \u043c\u043e\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u0432\u044a\u0432\u0435\u0434\u0435\u043d \u043a\u043e\u0440\u0435\u043a\u0442\u043d\u043e",
"unblock_btn": "\u041e\u0442\u0431\u043b\u043e\u043a\u0438\u0440\u0430\u0439",
"block_btn": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439",
"time_table_header": "\u0412\u0440\u0435\u043c\u0435",
"domain_name_table_header": "\u0418\u043c\u0435 \u043d\u0430 \u0434\u043e\u043c\u0435\u0439\u043d",
"type_table_header": "\u0422\u0438\u043f",
"response_table_header": "\u041e\u0442\u0433\u043e\u0432\u043e\u0440",
"client_table_header": "\u041a\u043b\u0438\u0435\u043d\u0442",
"empty_response_status": "\u041f\u0440\u0430\u0437\u0435\u043d",
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0436\u0438 \u0432\u0441\u0438\u0447\u043a\u0438",
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0436\u0438 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0438",
"no_logs_found": "\u041d\u044f\u043c\u0430 \u0438\u0441\u0442\u043e\u0440\u0438\u044f",
"disabled_log_btn": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"download_log_file_btn": "\u0421\u043c\u044a\u043a\u043d\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"refresh_btn": "\u041e\u0431\u043d\u043e\u0432\u0438",
"enabled_log_btn": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"last_dns_queries": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 5000 DNS \u0437\u0430\u044f\u0432\u043a\u0438",
"previous_btn": "\u041f\u0440\u0435\u0434\u0445\u043e\u0434\u0435\u043d",
"next_btn": "\u0421\u043b\u0435\u0434\u0432\u0430\u0449",
"loading_table_status": "\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435...",
"page_table_footer_text": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
"of_table_footer_text": "\u043e\u0442",
"rows_table_footer_text": "\u0440\u0435\u0434\u043e\u0432\u0435",
"updated_custom_filtering_toast": "\u041e\u0431\u043d\u043e\u0432\u0435\u043d\u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"rule_removed_from_custom_filtering_toast": "\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e \u043e\u0442 \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"rule_added_to_custom_filtering_toast": "\u0414\u043e\u0431\u0430\u0432\u0435\u043d\u043e \u0434\u043e \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"query_log_disabled_toast": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d\u0430",
"query_log_enabled_toast": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430",
"source_label": "\u0418\u0437\u0442\u043e\u0447\u043d\u0438\u043a",
"found_in_known_domain_db": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0441\u043f\u0438\u0441\u044a\u0446\u0438\u0442\u0435 \u0441 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
"filter_label": "\u0424\u0438\u043b\u0442\u044a\u0440",
"unknown_filter": "\u041d\u0435\u043f\u043e\u0437\u043d\u0430\u0442 \u0444\u0438\u043b\u0442\u044a\u0440 {{filterId}}",
"install_welcome_title": "\u0414\u043e\u0431\u0440\u0435 \u0434\u043e\u0448\u043b\u0438 \u0432 AdGuard Home!",
"install_welcome_desc": "AdGuard Home e \u043c\u0440\u0435\u0436\u043e\u0432\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0437\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0440\u0435\u043a\u043b\u0430\u043c\u0438 \u0438 \u0442\u0440\u0430\u043a\u0435\u0440\u0438 \u043d\u0430 DNS \u043d\u0438\u0432\u043e. \u0421\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u043e \u0435 \u0437\u0430 \u0434\u0430 \u0432\u0438 \u0434\u0430\u0434\u0435 \u043f\u044a\u043b\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430\u0434 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u0438 \u0432\u0441\u0438\u0447\u043a\u0438\u0442\u0435 \u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0431\u0435\u0437 \u0434\u0430 \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0434\u0440\u0443\u0433 \u0441\u043e\u0444\u0442\u0443\u0435\u0440.",
"install_settings_title": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f",
"install_settings_listen": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0438",
"install_settings_port": "\u041f\u043e\u0440\u0442",
"install_settings_interface_link": "\u0412\u0430\u0448\u0430\u0442\u0430 AdGuard Home \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0449\u0435 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043d\u0430 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441:",
"form_error_port": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043f\u043e\u0440\u0442",
"install_settings_dns": "DNS \u0441\u044a\u0440\u0432\u044a\u0440",
"install_settings_dns_desc": "\u0417\u0430 \u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u0438, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 \u0440\u0443\u0442\u0435\u0440 \u0438\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442 DNS \u0441\u044a\u0440\u0432\u044a\u0440 \u0441 \u0430\u0434\u0440\u0435\u0441:",
"install_settings_all_interfaces": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0438",
"install_auth_title": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435",
"install_auth_desc": "\u041c\u043d\u043e\u0433\u043e \u0435 \u0432\u0430\u0436\u043d\u043e \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0432\u0430\u0448\u0438\u044f \u043f\u0430\u043d\u0435\u043b \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home. \u041f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0432\u0438 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u0447\u0435 \u0433\u043e \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0432 \u043a\u044a\u0449\u0438.",
"install_auth_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
"install_auth_password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"install_auth_confirm": "\u041f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430",
"install_auth_username_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
"install_auth_password_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430",
"install_step": "\u0421\u0442\u044a\u043f\u043a\u0430",
"install_devices_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0435\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e",
"install_devices_desc": "\u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 AdGuard Home, \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_submit_title": "\u041f\u043e\u0437\u0434\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f!",
"install_submit_desc": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430 \u0435 \u0437\u0430\u0432\u044a\u0440\u0448\u0435\u043d\u0430, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 AdGuard Home.",
"install_devices_router": "\u0420\u0443\u0442\u0435\u0440",
"install_devices_router_desc": "\u0410\u043a\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 \u0440\u0443\u0442\u0435\u0440 \u043d\u044f\u043c\u0430 \u043d\u0443\u0436\u0434\u0430 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u0442\u0435 \u0432\u0441\u044f\u043a\u043e \u0435\u0434\u043d\u043e \u043e\u0442 \u0443\u0441\u0442\u0440\u0439\u0441\u0442\u0432\u0430\u0442\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.",
"install_devices_address": "AdGuard Home DNS \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u043d\u0430 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441",
"install_devices_router_list_1": "\u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0440\u0443\u0442\u0435\u0440. \u041e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u0442\u044f \u0441\u0435 \u043d\u0430\u043c\u0438\u0440\u0430 \u043d\u0430 URL (\u0442\u0443\u043a http:\/\/192.168.0.1\/ \u0438\u043b\u0438 \u0442\u0443\u043a http:\/\/192.168.1.1\/). \u0417\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u043c\u043e\u0436\u0435 \u0434\u0430 \u0432\u0438 \u0442\u0440\u044f\u0431\u0432\u0430 \u043f\u0430\u0440\u043e\u043b\u0430. \u0410\u043a\u043e \u0441\u0442\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u044f \u0440\u0435\u0441\u0435\u0442\u043d\u0435\u0442\u0435 \u043a\u0430\u0442\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0430 \u0441\u043a\u0440\u0438\u0442\u0438\u044f \u0440\u0435\u0441\u0435\u0442 \u0431\u0443\u0442\u043e\u043d - \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u0442\u043e\u0432\u0430 \u0449\u0435 \u0440\u0435\u0441\u0435\u0442\u043d\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0440\u0443\u0442\u0435\u0440\u0430 \u0434\u043e \u0444\u0430\u0431\u0440\u0438\u0447\u043d\u0438! \u041d\u044f\u043a\u043e\u0439 \u0440\u0443\u0442\u0435\u0440\u0438 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0438 \u043e\u0442 \u0441\u043e\u0444\u0442\u0443\u0435\u0440 \u0438\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0439\u0442\u043e \u0431\u0438 \u0442\u0440\u044f\u0431\u0432\u0430\u043b\u043e \u0434\u0430 \u0435 \u0432\u0435\u0447\u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d \u043d\u0430 \u043a\u043e\u043c\u043f\u044e\u0442\u044a\u0440\u0430\/\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432\u0438.",
"install_devices_router_list_2": "\u041d\u0430\u043c\u0435\u0440\u0435\u0442\u0430 DHCP\/DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412 \u043f\u043e\u0434 \u0440\u0430\u0437\u0434\u0435\u043b DHCP \u0440\u0437\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435 \u0438 \u043d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u043a\u044a\u0434\u0435 \u0435 \u043f\u043e\u043b\u0435\u0442\u043e \u0437\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432 \u043a\u043e\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438.",
"install_devices_router_list_3": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_windows_list_1": "\u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043d\u0438\u044f \u041f\u0430\u043d\u0435\u043b \u043f\u0440\u0435\u0437 \u0421\u0442\u0430\u0440\u0442 \u043c\u0435\u043d\u044e \u0438\u043b\u0438 \u0447\u0440\u0435\u0437 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0442\u044a\u0440\u0441\u0435\u043d\u0435 \u043d\u0430 Windows.",
"install_devices_windows_list_2": "\u0412\u044a\u0440\u0432\u0435\u0442\u0435 \u0434\u043e \u041d\u0430\u0441\u0442\u0440\u0439\u043a\u0438 \u043d\u0430 \u041c\u0440\u0435\u0436\u0438 \u0438 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0438 \u043e\u0442 \u0442\u0430\u043c \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u041c\u0440\u0435\u0436\u0438 \u0438 \u0426\u0435\u043d\u0442\u044a\u0440 \u0437\u0430 \u0421\u043f\u043e\u0434\u0435\u043b\u044f\u043d\u0435.",
"install_devices_windows_list_3": "\u041e\u0442 \u043b\u044f\u0432\u043e \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u0421\u043c\u0435\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u043a\u0438 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438\u044f \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u043d\u0435\u0433\u043e.",
"install_devices_windows_list_4": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0442\u043e\u0437\u0438 \u043a\u043e\u0439\u0442\u043e \u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d, \u0434\u044f\u0441\u043d\u043e-\u043a\u043b\u0438\u043a\u0432\u0430\u043d\u0435 \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0430 \u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_devices_windows_list_5": "\u041d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0412\u0435\u0440\u0441\u0438\u044f 4 (TCP\/IP) \u0432 \u0441\u043f\u0438\u0441\u044a\u043a\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043d\u0430 \u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_devices_windows_list_6": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0438 \u0437\u0430 DNS \u0441\u044a\u0440\u0441\u044a\u0440\u0438 \u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0432\u0438.",
"install_devices_macos_list_1": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Apple \u0438\u043a\u043e\u043d\u043a\u0430\u0442\u0430 \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 System Preferences...",
"install_devices_macos_list_2": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Network.",
"install_devices_macos_list_3": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0437\u0435\u043b\u0435\u043d\u0430\u0442\u0430-\u0430\u043a\u0442\u0438\u0432\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0432 \u0441\u043f\u0438\u0441\u044a\u043a\u0430 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Advanced.",
"install_devices_macos_list_4": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DNS \u0442\u0430\u0431 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 + \u0437\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_android_list_1": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Android \u041c\u0435\u043d\u044e \u043e\u0442 \u0434\u043e\u043c\u0430\u0448\u043d\u0438\u044f \u0435\u043a\u0440\u0430\u043d, \u0438 \u0446\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.",
"install_devices_android_list_2": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Wi-Fi \u043c\u0435\u043d\u044e. \u041d\u0430 \u0435\u043a\u0440\u0430\u043d\u0430 \u0449\u0435 \u0441\u0435 \u043f\u043e\u044f\u0432\u0430\u0442 \u0432\u0441\u0438\u0447\u043a\u0438 \u0431\u0435\u0437\u0436\u0438\u0447\u043d\u0438 \u043f\u0440\u0435\u0436\u0438 (\u0442\u0430\u043c \u043d\u044f\u043c\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0432\u044a\u0432\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438).",
"install_devices_android_list_3": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u0438 \u0437\u0430\u0434\u0440\u044a\u0436\u0434\u0435 \u0432\u044a\u0440\u0445\u0443 \u0412\u0438\u0435 \u0441\u0442\u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441.., \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u041c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0439 \u043c\u0440\u0435\u0436\u0430.",
"install_devices_android_list_4": "\u041d\u0430 \u043d\u044f\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0435 \u043d\u0435\u043e\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u0430 \u043c\u0430\u0440\u043a\u0438\u0440\u0430\u0442\u0435 \u043f\u043e\u043a\u0430\u0436\u0438 \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438, \u0437\u0430 \u0434\u0430 \u0432\u0438\u0434\u0438\u0442\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0417\u0430 \u0434\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 Android DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043d\u0430\u043b\u043e\u0436\u0438 \u0434\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 IP \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043e\u0442 DHCP \u043d\u0430 \u0421\u0442\u0430\u0442\u0438\u0447\u043d\u0438.",
"install_devices_android_list_5": "\u041f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442\u0438\u0442\u0435 \u043d\u0430 DNS 1 \u0438 DNS 2 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_ios_list_1": "\u041e\u0442 \u043d\u0430\u0447\u0430\u043b\u0435\u043d \u0435\u043a\u0440\u0430\u043d, \u0446\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Settings.",
"install_devices_ios_list_2": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Wi-Fi \u043e\u0442 \u043b\u044f\u0432\u043e\u0442\u043e \u043c\u0435\u043d\u044e (\u0442\u0430\u043c \u043d\u044f\u043c\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0432\u044a\u0432\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438).",
"install_devices_ios_list_3": "\u041a\u043b\u0438\u043d\u0435\u0442\u0435 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0430\u043a\u0442\u0438\u0432\u043d\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u043a\u044a\u043c \u043a\u043e\u044f\u0442\u043e \u0441\u0442\u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438.",
"install_devices_ios_list_4": "\u0412 \u043f\u043e\u043b\u0435\u0442\u043e \u0437\u0430 DNS \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u043e \u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"get_started": "\u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u043c\u0435",
"next": "\u0421\u043b\u0435\u0434\u0432\u0430\u0449",
"open_dashboard": "\u041e\u0442\u0432\u043e\u0440\u0438 \u0442\u0430\u0431\u043b\u043e",
"install_saved": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u043e",
"encryption_title": "\u041a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435",
"encryption_desc": "\u041f\u043e\u0434\u044a\u0440\u0436\u0430 \u0441\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 (HTTPS\/TLS) \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430 DNS \u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f",
"encryption_config_saved": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0430",
"encryption_server": "\u0418\u043c\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"encryption_server_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u043d\u0430 \u0434\u043e\u043c\u0435\u0439\u043d\u0430",
"encryption_server_desc": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 HTTPS, \u0442\u0440\u044f\u0431\u0432\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430 \u0441 \u0442\u043e\u0432\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430.",
"encryption_redirect": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043a\u044a\u043c HTTPS",
"encryption_redirect_desc": "\u0421\u043b\u0443\u0436\u0438 \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043e\u0442 HTTP \u043a\u044a\u043c HTTPS \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0432 AdGuard Home.",
"encryption_https": "HTTPS \u043f\u043e\u0440\u0442",
"encryption_https_desc": "\u0410\u043a\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 HTTPS \u043f\u043e\u0440\u0442, \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home \u0449\u0435 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043d\u0430 HTTPS, \u0438 \u0441\u044a\u0449\u043e \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u0430\u0440\u044f \u043d\u0430 DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS '\/dns-\u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f'.",
"encryption_dot": "DNS-\u0432\u044a\u0440\u0445\u0443-TLS \u043f\u043e\u0440\u0442",
"encryption_dot_desc": "\u0410\u043a\u043e \u043f\u043e\u0440\u0442\u0430 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d, AdGuard Home \u0449\u0435 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430 \u0438 \u0441\u044a\u0440\u0432\u044a\u0440 \u0437\u0430 DNS-\u0432\u044a\u0440\u0445\u0443-TLS.",
"encryption_certificates": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438",
"encryption_certificates_desc": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043e\u0441\u0438\u0433\u0443\u0440\u0438\u0442\u0435 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0437\u0430 \u0432\u0430\u0448\u0438\u044f \u0434\u043e\u043c\u0435\u0439\u043d. \u041c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u044f\u0432\u0438\u0442\u0435 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u0435\u043d \u043e\u0442 <0>{{link}}<\/0> \u0438\u043b\u0438 \u0434\u0430 \u0437\u0430\u043a\u0443\u043f\u0438\u0442\u0435 \u043e\u0442 Certificate Authorities.",
"encryption_certificates_input": "\u041a\u043e\u043f\u0438\u0440\u0430\u0439\/\u043f\u043e\u0441\u0442\u0430\u0432\u0438 \u0432\u0430\u0448\u0438\u044f PEM-\u043a\u043e\u0434\u0438\u0440\u0430\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0442\u0443\u043a.",
"encryption_status": "\u0421\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
"encryption_expire": "\u0413\u043e\u0434\u0435\u043d \u0434\u043e",
"encryption_key": "\u0427\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_key_input": "\u041a\u043e\u043f\u0438\u0440\u0430\u0439\/\u043f\u043e\u0441\u0442\u0430\u0432\u0438 \u0432\u0430\u0448\u0438\u044f PEM-\u043a\u043e\u0434\u0438\u0440\u0430\u043d \u0447p\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447 \u0437\u0430 \u0432\u0430\u0448\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0442\u0443\u043a.",
"encryption_enable": "\u0420\u0430\u0437p\u0435\u0448\u0438 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435 (HTTPS, DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS, \u0438 DNS-\u0432\u044a\u0440\u0445\u0443-TLS)",
"encryption_enable_desc": "\u0410\u043a\u043e \u0441\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u043b\u0438 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435, \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home \u0449\u0435 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043f\u0440\u0435\u0437 HTTPS, \u0438 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u0430\u0440\u044f \u0441\u044a\u0449\u043e \u043d\u0430 \u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS \u0438 DNS-\u0432\u044a\u0440\u0445\u0443-TLS.",
"encryption_chain_valid": "\u0419\u0435\u0440\u0430\u0440\u0445\u0438\u044f\u0442\u0430 \u043e\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0435 \u0432\u0430\u043b\u0438\u0434\u043d\u0430",
"encryption_chain_invalid": "\u0419\u0435\u0440\u0430\u0440\u0445\u0438\u044f\u0442\u0430 \u043e\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430",
"encryption_key_valid": "\u0422\u043e\u0432\u0430 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d {{type}} \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_key_invalid": "\u0422\u043e\u0432\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d {{type}} \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_subject": "\u0422\u0435\u043c\u0430",
"encryption_issuer": "\u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b",
"encryption_hostnames": "\u0418\u043c\u0435\u043d\u0430 \u043d\u0430 \u0445\u043e\u0441\u0442\u0430",
"encryption_reset": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435 \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435?",
"topline_expiring_certificate": "\u0412\u0430\u0448\u0438\u044f\u0442 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438\u0437\u0442\u0438\u0447\u0430. \u041e\u0431\u043d\u043e\u0432\u0438 <0>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435<\/0>.",
"topline_expired_certificate": "\u0412\u0430\u0448\u0438\u044f\u0442 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0435 \u0438\u0437\u0442\u0435\u043a\u044a\u043b. \u041e\u0431\u043d\u043e\u0432\u0438 <0>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435<\/0>.",
"form_error_port_range": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0440\u0442 \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430 80-65535",
"form_error_port_unsafe": "\u041d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u043e\u0440\u0442",
"form_error_equal": "\u041d\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430",
"form_error_password": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0435 \u0441\u044a\u0432\u043f\u0430\u0434\u0430",
"reset_settings": "\u0418\u0437\u0442\u0440\u0438\u0439 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"update_announcement": "\u0418\u043c\u0430 \u043d\u043e\u0432\u0430 AdGuard Home {{version}}! <0>\u0426\u044a\u043a\u043d\u0438 \u0442\u0443\u043a<\/0> \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f."
}

View File

@@ -1,5 +1,9 @@
{
"url_added_successfully": "Url added successfully",
"example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)<\/0>",
"upstream_parallel": "Use parallel queries to speed up resolving by simultaneously querying all upstream servers",
"bootstrap_dns": "Bootstrap DNS servers",
"bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH\/DoT resolvers you specify as upstreams.",
"url_added_successfully": "URL added successfully",
"check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save config",
"enabled_dhcp": "DHCP server enabled",
@@ -8,8 +12,8 @@
"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": "Some active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
"dhcp_not_found": "It is safe to enable the built-in DHCP server - we didn't find any active DHCP servers on the network. However, we encourage you to re-check it manually as our automatic test currently doesn't give 100% guarantee.",
"dhcp_found": "An active DHCP server is 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",
@@ -28,7 +32,11 @@
"dhcp_ip_addresses": "IP addresses",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expires",
"dhcp_warning": "If you want to enable the built-in DHCP server, make sure that there is no other active DHCP server. Otherwise, it can break the internet for connected devices!",
"dhcp_warning": "If you want to enable DHCP server anyway, make sure that there is no other active DHCP server in your network. Otherwise, it can break the Internet for connected devices!",
"dhcp_error": "We could not determine whether there is another DHCP server in the network.",
"dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. We will automatically set this IP address as static if you press Enable DHCP button.",
"error_details": "Error details",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
@@ -42,6 +50,7 @@
"copyright": "Copyright",
"homepage": "Homepage",
"report_an_issue": "Report an issue",
"privacy_policy": "Privacy policy",
"enable_protection": "Enable protection",
"enabled_protection": "Enabled protection",
"disable_protection": "Disable protection",
@@ -74,12 +83,12 @@
"use_adguard_parental": "Use AdGuard parental control web service",
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
"enforce_safe_search": "Enforce safe search",
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, and Yandex.",
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo and Yandex.",
"no_servers_specified": "No servers specified",
"no_settings": "No settings",
"general_settings": "General settings",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream. Use tls:\/\/ prefix for DNS over TLS servers.",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream.",
"test_upstream_btn": "Test upstreams",
"apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering",
@@ -118,9 +127,9 @@
"example_comment_hash": "# Also a comment",
"example_regex_meaning": "block access to the domains matching the specified regular expression",
"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_dot": "encrypted <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "you can use <0>DNS Stamps<\/0> for <1>DNSCrypt<\/1> or <2>DNS-over-HTTPS<\/2> 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",
@@ -211,11 +220,11 @@
"open_dashboard": "Open Dashboard",
"install_saved": "Saved successfully",
"encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS/TLS) support for both DNS and admin web interface",
"encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need yo enter the server name that matches your SSL certificate.",
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port",
@@ -223,12 +232,12 @@
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy/paste your PEM-encoded cerificates here.",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}<\/0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy\/paste your PEM-encoded certificates here.",
"encryption_status": "Status",
"encryption_expire": "Expires",
"encryption_key": "Private key",
"encryption_key_input": "Copy/paste your PEM-encoded private key for your cerficate here.",
"encryption_key_input": "Copy\/paste your PEM-encoded private key for your certificate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid",
@@ -239,12 +248,16 @@
"encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings<\/0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings<\/0>.",
"form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal",
"form_error_password": "Password mismatched",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info."
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
"setup_guide": "Setup guide",
"dns_addresses": "DNS addresses",
"down": "Down",
"fix": "Fix"
}

View File

@@ -1,132 +1,139 @@
{
"example_upstream_reserved": "puede especificar el DNS de subida <0>para un dominio espec\u00edfico<\/0>",
"upstream_parallel": "Usar consultas paralelas para acelerar la resoluci\u00f3n al consultar simult\u00e1neamente a todos los servidores de subida",
"bootstrap_dns": "Servidores DNS de arranque",
"bootstrap_dns_desc": "Los servidores DNS de arranque se utilizan para resolver las direcciones IP de los resolutores DoH\/DoT que especifique como DNS de subida.",
"url_added_successfully": "URL a\u00f1adida correctamente",
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
"save_config": "Guardar config",
"save_config": "Guardar configuraci\u00f3n",
"enabled_dhcp": "Servidor DHCP habilitado",
"disabled_dhcp": "Servidor DHCP deshabilitado",
"dhcp_title": "Servidor DHCP (experimental)",
"dhcp_description": "Si su enrutador no proporciona la configuraci\u00f3n DHCP, puede utilizar el propio servidor DHCP incorporado de AdGuard.",
"dhcp_description": "Si su router 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_found": "Algunos servidores DHCP activos se encuentran 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_gateway_input": "IP de puerta de enlace",
"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_form_lease_input": "Duraci\u00f3n de la concesi\u00f3n",
"dhcp_interface_select": "Seleccione la interfaz DHCP",
"dhcp_hardware_address": "Direcci\u00f3n de hardware",
"dhcp_hardware_address": "Direcci\u00f3n MAC",
"dhcp_ip_addresses": "Direcciones IP",
"dhcp_table_hostname": "Hostname",
"dhcp_table_hostname": "Nombre del host",
"dhcp_table_expires": "Expira",
"dhcp_warning": "Si desea habilitar el servidor DHCP incorporado, aseg\u00farese de que no hay otro servidor DHCP activo. \u00a1De lo contrario, puede dejar sin Internet a los dispositivos conectados!",
"back": "Atr\u00e1s",
"dashboard": "Tablero de rendimiento",
"settings": "Ajustes",
"dashboard": "Panel de control",
"settings": "Configuraci\u00f3n",
"filters": "Filtros",
"query_log": "Log de consulta",
"faq": "FAQ",
"query_log": "Registro de consultas",
"faq": "Preguntas frecuentes",
"version": "versi\u00f3n",
"address": "direcci\u00f3n",
"on": "Activado",
"off": "Desactivado",
"copyright": "Derechos de autor",
"copyright": "Copyright",
"homepage": "P\u00e1gina de inicio",
"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",
"enable_protection": "Habilitar protecci\u00f3n",
"enabled_protection": "Protecci\u00f3n habilitada",
"disable_protection": "Deshabilitar protecci\u00f3n",
"disabled_protection": "Protecci\u00f3n deshabilitada",
"refresh_statics": "Restablecer estad\u00edsticas",
"dns_query": "Consultas DNS",
"blocked_by": "Bloqueado por filtros",
"stats_malware_phishing": "Malware\/phishing bloqueado",
"stats_adult": "Contenido para adultos bloqueado",
"stats_query_domain": "Dominios m\u00e1s solicitados",
"stats_adult": "Sitios para adultos bloqueado",
"stats_query_domain": "Dominios m\u00e1s consultados",
"for_last_24_hours": "en las \u00faltimas 24 horas",
"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",
"top_clients": "Clientes m\u00e1s frecuentes",
"no_clients_found": "No hay clientes",
"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 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",
"number_of_dns_query_24_hours": "N\u00famero de consultas DNS procesadas durante las \u00faltimas 24 horas",
"number_of_dns_query_blocked_24_hours": "N\u00famero de peticiones DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "N\u00famero de peticiones DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "N\u00famero de sitios para adultos bloqueado",
"enforced_save_search": "B\u00fasquedas seguras forzadas",
"number_of_dns_query_to_safe_search": "N\u00famero de peticiones DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la b\u00fasqueda segura forzada",
"average_processing_time": "Tiempo promedio de procesamiento",
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una solicitud DNS",
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una petici\u00f3n 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 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 ",
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en la configuraci\u00f3n de <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 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: solo se env\u00eda al servidor un prefijo corto de hash del nombre de dominio SHA256.",
"use_adguard_parental": "Usar el control parental de AdGuard",
"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.",
"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",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como upstream. Utilice el prefijo tls:\/\/ para DNS sobre servidores TLS.",
"test_upstream_btn": "Probar upstream",
"no_settings": "Sin configuraci\u00f3n",
"general_settings": "Configuraci\u00f3n general",
"upstream_dns": "Servidores DNS de subida",
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como DNS de subida. Utilice el prefijo tls:\/\/ para los servidores DNS mediante TLS.",
"test_upstream_btn": "Probar DNS de subida",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Desactivar filtrado",
"enabled_filtering_toast": "Filtrado activado",
"disabled_safe_browsing_toast": "Navegaci\u00f3n segura desactivada",
"enabled_safe_browsing_toast": "Navegaci\u00f3n segura activada",
"disabled_parental_toast": "Control parental desactivado",
"enabled_parental_toast": "Control parental activado",
"disabled_safe_search_toast": "B\u00fasqueda segura desactivada",
"enabled_save_search_toast": "B\u00fasqueda segura activada",
"enabled_table_header": "Activado",
"disabled_filtering_toast": "Filtrado deshabilitado",
"enabled_filtering_toast": "Filtrado habilitado",
"disabled_safe_browsing_toast": "B\u00fasqueda segura deshabilitada",
"enabled_safe_browsing_toast": "B\u00fasqueda segura habilitada",
"disabled_parental_toast": "Control parental deshabilitado",
"enabled_parental_toast": "Control parental habilitado",
"disabled_safe_search_toast": "B\u00fasqueda segura deshabilitada",
"enabled_save_search_toast": "B\u00fasqueda segura habilitada",
"enabled_table_header": "Habilitado",
"name_table_header": "Nombre",
"filter_url_table_header": "Filtro URL",
"filter_url_table_header": "URL del filtro",
"rules_count_table_header": "N\u00famero de reglas",
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
"actions_table_header": "Acciones",
"delete_table_action": "Eliminar",
"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",
"no_filters_added": "No hay filtros a\u00f1adidos",
"add_filter_btn": "A\u00f1adir filtro",
"cancel_btn": "Cancelar",
"enter_name_hint": "Ingresar nombre",
"enter_url_hint": "Ingresar URL",
"check_updates_btn": "Revisar si hay actualizaciones",
"enter_name_hint": "Ingrese el nombre",
"enter_url_hint": "Ingrese la URL",
"check_updates_btn": "Buscar actualizaciones",
"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.",
"custom_filter_rules_hint": "Ingrese una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o la sintaxis de archivos de hosts.",
"examples_title": "Ejemplos",
"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_filter_block": "bloquea el acceso al dominio ejemplo.org\ny a todos sus subdominios",
"example_meaning_filter_whitelist": "desbloquea el acceso al dominio ejemplo.org y a todos 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 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 actualizados",
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
"example_regex_meaning": "bloquear el acceso a los dominios que coincidan con la expresi\u00f3n regular especificada",
"example_upstream_regular": "DNS regular (mediante UDP)",
"example_upstream_dot": "cifrado <0>DNS mediante TLS<\/0>",
"example_upstream_doh": "cifrado <0>DNS mediante HTTPS<\/0>",
"example_upstream_sdns": "puedes usar <0>DNS Stamps<\/0> para <1>DNSCrypt<\/1> o resolutores <2>DNS mediante HTTPS<\/2>",
"example_upstream_tcp": "DNS regular (mediante TCP)",
"all_filters_up_to_date_toast": "Todos los filtros ya est\u00e1n actualizados",
"updated_upstream_dns_toast": "Servidores DNS de subida actualizados",
"dns_test_ok_toast": "Los 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",
"block_btn": "Bloquear",
"time_table_header": "Tiempo",
"domain_name_table_header": "Nombre de dominio",
"time_table_header": "Hora",
"domain_name_table_header": "Nombre del dominio",
"type_table_header": "Tipo",
"response_table_header": "Respuesta",
"client_table_header": "Cliente",
@@ -134,26 +141,116 @@
"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": "Refrescar",
"enabled_log_btn": "Activar registro",
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
"previous_btn": "Anterior",
"disabled_log_btn": "Deshabilitar registro",
"download_log_file_btn": "Descargar archivo de registro",
"refresh_btn": "Actualizar",
"enabled_log_btn": "Habilitar registro",
"last_dns_queries": "\u00daltimas 5000 consultas DNS",
"previous_btn": "Atr\u00e1s",
"next_btn": "Siguiente",
"loading_table_status": "Cargando...",
"page_table_footer_text": "P\u00e1gina",
"of_table_footer_text": "de",
"rows_table_footer_text": "filas",
"updated_custom_filtering_toast": "Actualizadas las reglas de filtrado personalizadas",
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizadas",
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizadas",
"query_log_disabled_toast": "Log de consulta desactivado",
"query_log_enabled_toast": "Log de consulta activado",
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado",
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizado",
"query_log_disabled_toast": "Registro de consultas deshabilitado",
"query_log_enabled_toast": "Registro de consultas habilitado",
"source_label": "Fuente",
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
"category_label": "Categor\u00eda",
"rule_label": "Regla",
"filter_label": "Filtro",
"unknown_filter": "Filtro desconocido {{filterId}}"
"unknown_filter": "Filtro desconocido {{filterId}}",
"install_welcome_title": "\u00a1Bienvenido a AdGuard Home!",
"install_welcome_desc": "AdGuard Home es un servidor DNS de bloqueo de anuncios y rastreadores en toda la red. Su prop\u00f3sito es permitirle controlar toda su red y todos sus dispositivos, y no requiere el uso de un programa del lado del cliente.",
"install_settings_title": "Interfaz web de administraci\u00f3n",
"install_settings_listen": "Interfaz de escucha",
"install_settings_port": "Puerto",
"install_settings_interface_link": "Su interfaz web de administraci\u00f3n de AdGuard Home estar\u00e1 disponible en las siguientes direcciones:",
"form_error_port": "Ingrese un valor de puerto v\u00e1lido",
"install_settings_dns": "Servidor DNS",
"install_settings_dns_desc": "Deber\u00e1 configurar sus dispositivos o router para usar el servidor DNS en las siguientes direcciones:",
"install_settings_all_interfaces": "Todas las interfaces",
"install_auth_title": "Autenticaci\u00f3n",
"install_auth_desc": "Se recomienda encarecidamente configurar la autenticaci\u00f3n por contrase\u00f1a para la interfaz web del administraci\u00f3n de AdGuard Home. Incluso si solo es accesible en su red local, es importante que est\u00e9 protegido contra el acceso no autorizado.",
"install_auth_username": "Usuario",
"install_auth_password": "Contrase\u00f1a",
"install_auth_confirm": "Confirmar contrase\u00f1a",
"install_auth_username_enter": "Ingrese su nombre de usuario",
"install_auth_password_enter": "Ingrese su contrase\u00f1a",
"install_step": "Paso",
"install_devices_title": "Configure sus dispositivos",
"install_devices_desc": "Para que AdGuard Home comience a funcionar, necesita configurar sus dispositivos para usarlo.",
"install_submit_title": "\u00a1Felicitaciones!",
"install_submit_desc": "El proceso de configuraci\u00f3n ha finalizado y est\u00e1 listo para comenzar a usar AdGuard Home.",
"install_devices_router": "Router",
"install_devices_router_desc": "Esta configuraci\u00f3n cubrir\u00e1 autom\u00e1ticamente todos los dispositivos conectados a su router dom\u00e9stico y no necesitar\u00e1 configurar cada uno de ellos manualmente.",
"install_devices_address": "El servidor DNS de AdGuard Home est\u00e1 escuchando las siguientes direcciones",
"install_devices_router_list_1": "Abra las preferencias de su router. Por lo general, puede acceder a \u00e9l desde su navegador a trav\u00e9s de una URL (como http:\/\/192.168.0.1\/ o http:\/\/192.168.1.1\/). Se le puede pedir que ingrese la contrase\u00f1a. Si no lo recuerda, a menudo puede restablecer la contrase\u00f1a presionando un bot\u00f3n en el router. Algunos routers requieren una aplicaci\u00f3n espec\u00edfica, que en ese caso ya deber\u00eda estar instalada en su computadora\/tel\u00e9fono.",
"install_devices_router_list_2": "Busque la configuraci\u00f3n de DHCP\/DNS. Busque las letras DNS junto a un campo que permita ingresar dos o tres grupos de n\u00fameros, cada uno dividido en cuatro grupos de uno a tres d\u00edgitos.",
"install_devices_router_list_3": "Ingrese las direcciones de su servidor AdGuard Home all\u00ed.",
"install_devices_windows_list_1": "Abra el Panel de control a trav\u00e9s del men\u00fa Inicio o en el buscador de Windows.",
"install_devices_windows_list_2": "Vaya a la categor\u00eda Red e Internet, luego al Centro de redes y recursos compartidos.",
"install_devices_windows_list_3": "En el lado izquierdo de la pantalla, busque Cambiar la configuraci\u00f3n del adaptador y haga clic en \u00e9l.",
"install_devices_windows_list_4": "Seleccione su conexi\u00f3n activa, luego haga clic derecho sobre ella y elija Propiedades.",
"install_devices_windows_list_5": "Busque el Protocolo de Internet versi\u00f3n 4 (TCP\/IP) en la lista, selecci\u00f3nelo y luego haga clic en Propiedades nuevamente.",
"install_devices_windows_list_6": "Elija Usar las siguientes direcciones de servidor DNS e ingrese las direcciones de su servidor AdGuard Home.",
"install_devices_macos_list_1": "Haga clic en el icono de Apple y vaya a Preferencias del sistema.",
"install_devices_macos_list_2": "Haga clic en Red.",
"install_devices_macos_list_3": "Seleccione la primera conexi\u00f3n de la lista y haga clic en Avanzado.",
"install_devices_macos_list_4": "Seleccione la pesta\u00f1a DNS e ingrese las direcciones de su servidor AdGuard Home.",
"install_devices_android_list_1": "En la pantalla de inicio del men\u00fa Android, pulse en Configuraci\u00f3n.",
"install_devices_android_list_2": "Pulse Wi-Fi en el men\u00fa. Aparecer\u00e1 la pantalla que lista todas las redes disponibles (es imposible configurar DNS personalizados para la conexi\u00f3n m\u00f3vil).",
"install_devices_android_list_3": "Mantenga presionada la red a la que est\u00e1 conectado y pulse Modificar red.",
"install_devices_android_list_4": "En algunos dispositivos, es posible que deba marcar la casilla Avanzado para ver m\u00e1s configuraciones. Para ajustar la configuraci\u00f3n DNS de su Android, deber\u00e1 cambiar la configuraci\u00f3n de IP de DHCP a Est\u00e1tica.",
"install_devices_android_list_5": "Cambie los valores de DNS 1 y DNS 2 a las direcciones de su servidor AdGuard Home.",
"install_devices_ios_list_1": "En la pantalla de inicio, pulse en Configuraci\u00f3n.",
"install_devices_ios_list_2": "Elija Wi-Fi en el men\u00fa de la izquierda (es imposible configurar DNS para redes m\u00f3viles).",
"install_devices_ios_list_3": "Pulse sobre el nombre de la red activa en ese momento.",
"install_devices_ios_list_4": "En ese campo DNS ingrese las direcciones de su servidor AdGuard Home.",
"get_started": "Comenzar",
"next": "Siguiente",
"open_dashboard": "Abrir panel de control",
"install_saved": "Guardado correctamente",
"encryption_title": "Cifrado",
"encryption_desc": "Soporte para cifrado (HTTPS\/TLS) tanto para DNS como para la interfaz web de administraci\u00f3n",
"encryption_config_saved": "Configuraci\u00f3n de cifrado guardado",
"encryption_server": "Nombre del servidor",
"encryption_server_enter": "Ingrese su nombre de dominio",
"encryption_server_desc": "Para utilizar HTTPS, debe ingresar el nombre del servidor que coincida con su certificado SSL.",
"encryption_redirect": "Redireccionar a HTTPS autom\u00e1ticamente",
"encryption_redirect_desc": "Si est\u00e1 marcado, AdGuard Home redireccionar\u00e1 autom\u00e1ticamente de HTTP a las direcciones HTTPS.",
"encryption_https": "Puerto HTTPS",
"encryption_https_desc": "Si el puerto HTTPS est\u00e1 configurado, la interfaz de administraci\u00f3n de AdGuard Home ser\u00e1 accesible a trav\u00e9s de HTTPS, y tambi\u00e9n proporcionar\u00e1 DNS mediante HTTPS en la ubicaci\u00f3n '\/dns-query'.",
"encryption_dot": "Puerto para DNS mediante TLS",
"encryption_dot_desc": "Si este puerto est\u00e1 configurado, AdGuard Home ejecutar\u00e1 un servidor DNS mediante TLS en este puerto.",
"encryption_certificates": "Certificados",
"encryption_certificates_desc": "Para utilizar el cifrado, debe proporcionar una cadena de certificados SSL v\u00e1lida para su dominio. Puede obtener un certificado gratuito en <0>{{link}}<\/0> o puede comprarlo en una de las autoridades de certificaci\u00f3n de confianza.",
"encryption_certificates_input": "Copie\/pegue aqu\u00ed sus certificados codificados PEM.",
"encryption_status": "Estado",
"encryption_expire": "Expira",
"encryption_key": "Clave privada",
"encryption_key_input": "Copie\/pegue aqu\u00ed su clave privada codificada PEM para su certificado.",
"encryption_enable": "Habilitar el cifrado (HTTPS, DNS mediante HTTPS y DNS mediante TLS)",
"encryption_enable_desc": "Si el cifrado est\u00e1 habilitado, la interfaz de administraci\u00f3n de AdGuard Home funcionar\u00e1 a trav\u00e9s de HTTPS, y el servidor DNS escuchar\u00e1 las peticiones DNS mediante HTTPS y DNS mediante TLS.",
"encryption_chain_valid": "La cadena de certificado es v\u00e1lida",
"encryption_chain_invalid": "La cadena de certificado no es v\u00e1lida",
"encryption_key_valid": "Esta es una clave privada {{type}} v\u00e1lida",
"encryption_key_invalid": "Esta es una clave privada {{type}} no v\u00e1lida",
"encryption_subject": "Asunto",
"encryption_issuer": "Emisor",
"encryption_hostnames": "Nombres de hosts",
"encryption_reset": "\u00bfEst\u00e1 seguro de que desea restablecer la configuraci\u00f3n de cifrado?",
"topline_expiring_certificate": "Su certificado SSL est\u00e1 a punto de expirar. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
"topline_expired_certificate": "Su certificado SSL ha expirado. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
"form_error_port_range": "Ingrese el valor del puerto en el rango de 80 - 65535",
"form_error_port_unsafe": "Este es un puerto inseguro",
"form_error_equal": "No deber\u00eda ser igual",
"form_error_password": "La contrase\u00f1a no coincide",
"reset_settings": "Restablecer configuraci\u00f3n",
"update_announcement": "\u00a1AdGuard Home {{version}} ya est\u00e1 disponible! <0>Haga clic aqu\u00ed<\/0> para m\u00e1s informaci\u00f3n.",
"setup_guide": "Gu\u00eda de configuraci\u00f3n",
"dns_addresses": "Direcciones DNS"
}

View File

@@ -116,9 +116,9 @@
"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_sdns": "vous pouvez utiliser <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> pour <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ou les resolveurs <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a>",
"example_upstream_dot": "<0>DNS-au-dessus-de-TLS<\/0> chiffr\u00e9",
"example_upstream_doh": "<0>DNS-au-dessus-de-HTTPS<\/0> chiffr\u00e9",
"example_upstream_sdns": "vous pouvez utiliser <0>DNS Stamps<\/0> pour <1>DNSCrypt<\/1> ou les resolveurs <2>DNS-au-dessus-de-HTTPS<\/2>",
"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",

View File

@@ -115,9 +115,9 @@
"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_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "<0>DNSCrypt<\/0> \u307e\u305f\u306f <1>DNS-over-HTTPS<\/1> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b <2>DNS Stamps<\/2> \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",

View File

@@ -1,6 +1,10 @@
{
"url_added_successfully": "Url adicionada com sucesso",
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
"example_upstream_reserved": "Voc\u00ea pode especificar um DNS upstream <0>para um dom\u00ednio(s) especifico<\/0>",
"upstream_parallel": "Usar consultas paralelas para acelerar a resolu\u00e7\u00e3o consultando simultaneamente todos os servidores upstream",
"bootstrap_dns": "Servidores DNS de inicializa\u00e7\u00e3o",
"bootstrap_dns_desc": "Servidores DNS de inicializa\u00e7\u00e3o s\u00e3o usados para resolver endere\u00e7os IP dos resolvedores DoH\/DoT que voc\u00ea especifica como upstreams.",
"url_added_successfully": "URL adicionada com sucesso",
"check_dhcp_servers": "Verificar por servidores DHCP",
"save_config": "Salvar configura\u00e7\u00e3o",
"enabled_dhcp": "Servidor DHCP ativado",
"disabled_dhcp": "Servidor DHCP desativado",
@@ -28,6 +32,7 @@
"dhcp_ip_addresses": "Endere\u00e7o de IP",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expira",
"dhcp_warning": "Se voc\u00ea quiser ativar o servidor DHCP interno, certifique-se que n\u00e3o h\u00e1 outro servidor DHCP ativo. Caso contr\u00e1rio, isso pode impedir que outros dispositivos conectem \u00e1 internet!",
"back": "Voltar",
"dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es",
@@ -78,7 +83,7 @@
"no_settings": "N\u00e3o configurado",
"general_settings": "Configura\u00e7\u00f5es gerais",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream. Use o prefixo tls:\/\/ para servidores DNS com TLS.",
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream.",
"test_upstream_btn": "Testar upstreams",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtragem desativada",
@@ -115,10 +120,11 @@
"example_comment": "! Aqui vai um coment\u00e1rio",
"example_comment_meaning": "apenas um coment\u00e1rio",
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
"example_regex_meaning": "bloqueia o acesso aos dom\u00ednios correspondentes \u00e0 express\u00e3o regular especificada",
"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_dot": "<0>DNS-sobre-TLS<\/0> criptografado",
"example_upstream_doh": "<0>DNS-sobre-HTTPS<\/0> criptografado",
"example_upstream_sdns": "Voc\u00ea pode usar <0>DNS Stamps<\/0>para o <1>DNSCrypt<\/1>ou usar os resolvedores <2>DNS-sobre-HTTPS<\/2>",
"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",
@@ -156,5 +162,95 @@
"category_label": "Categoria",
"rule_label": "Regra",
"filter_label": "Filtro",
"unknown_filter": "Filtro desconhecido {{filterId}}"
"unknown_filter": "Filtro desconhecido {{filterId}}",
"install_welcome_title": "Bem-vindo(a) ao AdGuard Home!",
"install_welcome_desc": "O AdGuard Home \u00e9 um servidor de DNS para bloqueio de an\u00fancios e rastreamento em toda a rede. Sua finalidade \u00e9 permitir que voc\u00ea controle toda a sua rede e seus dispositivos sem precisar ter um programa instalado.",
"install_settings_title": "Interface web de administrador",
"install_settings_listen": "Interface de escuta",
"install_settings_port": "Porta",
"install_settings_interface_link": "A interface web de administrador do AdGuard estar\u00e1 dispon\u00edvel nos seguintes endere\u00e7os:",
"form_error_port": "Digite uma porta v\u00e1lida",
"install_settings_dns": "Servidor DNS",
"install_settings_dns_desc": "Voc\u00ea precisa configurar seu dispositivo ou roteador para usar o servidor DNS nos seguintes endere\u00e7os:",
"install_settings_all_interfaces": "Todas interfaces",
"install_auth_title": "Autentica\u00e7\u00e3o",
"install_auth_desc": "\u00c9 altamente recomend\u00e1vel configurar uma senha de autentica\u00e7\u00e3o na interface web de administrador do AdGuard Home. Mesmo que seja acess\u00edvel apenas em sua rede local, ainda \u00e9 importante proteg\u00ea-lo contra o acesso irrestrito.",
"install_auth_username": "Nome de usu\u00e1rio",
"install_auth_password": "Senha",
"install_auth_confirm": "Confirmar senha",
"install_auth_username_enter": "Digite o nome de usu\u00e1rio",
"install_auth_password_enter": "Digite a senha",
"install_step": "Passo",
"install_devices_title": "Configure seus dispositivos",
"install_devices_desc": "Para que o AdGuard Home comece a funcionar, voc\u00ea precisa configurar seus dispositivos para us\u00e1-lo.",
"install_submit_title": "Parab\u00e9ns!",
"install_submit_desc": "O procedimento de configura\u00e7\u00e3o est\u00e1 conclu\u00eddo e voc\u00ea est\u00e1 pronto para come\u00e7ar a usar o AdGuard Home.",
"install_devices_router": "Roteador",
"install_devices_router_desc": "Esta configura\u00e7\u00e3o cobrir\u00e1 automaticamente todos os dispositivos conectados ao seu roteador dom\u00e9stico e voc\u00ea n\u00e3o ir\u00e1 precisar configurar cada um deles manualmente.",
"install_devices_address": "O servidor de DNS do AdGuard Home est\u00e1 capturando os seguintes endere\u00e7os",
"install_devices_router_list_1": "Abra as configura\u00e7\u00f5es do seu roteador\nNo navegador digite o IP do roteador, o padr\u00e3o \u00e9 (http:\/\/192.168.0.1\/ ou http:\/\/192.168.1.1\/), e o login e senha \u00e9 admin\/admin; Se voc\u00ea n\u00e3o se lembra da senha, voc\u00ea pode redefinir a senha rapidamente pressionando um bot\u00e3o no pr\u00f3prio roteador. Alguns roteadores t\u00eam um aplicativo espec\u00edfico que j\u00e1 deve estar instalado em seu computador\/telefone.",
"install_devices_router_list_2": "Encontre as Configura\u00e7\u00f5es de DNS. Procure as letras DNS ao lado de um campo que permite dois ou tr\u00eas conjuntos de n\u00fameros, cada um dividido em quatro grupos de um a tr\u00eas n\u00fameros.",
"install_devices_router_list_3": "Digite aqui seu servidor do AdGuard Home.",
"install_devices_windows_list_1": "Abra o Painel de Controle pelo Menu Iniciar ou pela Pesquisa do Windows.",
"install_devices_windows_list_2": "Entre na categoria Rede e Internet e depois clique em Central de Rede e Compartilhamento.",
"install_devices_windows_list_3": "No lado esquerdo da janela clique em Alterar as configura\u00e7\u00f5es do adaptador.",
"install_devices_windows_list_4": "Selecione sua atual conex\u00e3o, clique nela com o bot\u00e3o direito do mouse e depois clique em Propriedades.",
"install_devices_windows_list_5": "Procure na lista por Internet Protocol Version 4 (TCP\/IP), selecione e clique em Propriedades novamente.",
"install_devices_windows_list_6": "Marque usar os seguintes endere\u00e7os de servidor DNS e digite os endere\u00e7os do servidores do AdGuard Home.",
"install_devices_macos_list_1": "Clique na \u00edcone da Apple e depois em Prefer\u00eancias do Sistema.",
"install_devices_macos_list_2": "Clique em Rede.",
"install_devices_macos_list_3": "Selecione a primeira conex\u00e3o da lista e clique em Avan\u00e7ado.",
"install_devices_macos_list_4": "Selecione a guia DNS e digite os endere\u00e7os dos servidores do AdGuard Home.",
"install_devices_android_list_1": "Na tela inicial do menu Android, toque em Configura\u00e7\u00f5es.",
"install_devices_android_list_2": "Toque em Wi-Fi. A tela listando todas as redes ser\u00e1 exibida (n\u00e3o \u00e9 poss\u00edvel configurar DNS personalizado para uma conex\u00e3o de dados m\u00f3veis)",
"install_devices_android_list_3": "Pressione prolongadamente a rede para a qual voc\u00ea est\u00e1 conectado e toque em Modificar rede",
"install_devices_android_list_4": "Em alguns dispositivos, talvez seja necess\u00e1rio marcar a caixa Avan\u00e7ado para ver as outras configura\u00e7\u00f5es. Para ajustar suas configura\u00e7\u00f5es de DNS do Android, voc\u00ea precisar\u00e1 alternar as configura\u00e7\u00f5es de IP de DHCP para Est\u00e1tico.",
"install_devices_android_list_5": "Altere o conjunto dos valores DNS 1 e DNS 2 para os endere\u00e7os de servidores do AdGuard Home.",
"install_devices_ios_list_1": "Na tela incial, toque em Ajustes.",
"install_devices_ios_list_2": "Selecione Wi-Fi no menu esquerdo (n\u00e3o \u00e9 poss\u00edvel configurar o DNS em conex\u00f5es de dados m\u00f3veis).",
"install_devices_ios_list_3": "Toque no nome da rede atualmente ativa.",
"install_devices_ios_list_4": "No campo DNS, digite os endere\u00e7os dos servidores do AdGuard Home.",
"get_started": "Come\u00e7ar",
"next": "Pr\u00f3ximo",
"open_dashboard": "Abrir painel",
"install_saved": "Salvo com sucesso",
"encryption_title": "Criptografia",
"encryption_desc": "Suporte a criptografia (HTTPS\/TLS) para DNS e interface de administra\u00e7\u00e3o web",
"encryption_config_saved": "Configura\u00e7\u00e3o de criptografia salva",
"encryption_server": "Nome do servidor",
"encryption_server_enter": "Digite seu nome de dom\u00ednio",
"encryption_server_desc": "Para usar o protocolo HTTPS, voc\u00ea precisa digitar o nome do servidor que corresponde ao seu certificado SSL.",
"encryption_redirect": "Redirecionar automaticamente para HTTPS",
"encryption_redirect_desc": "Se marcado, o AdGuard Home ir\u00e1 redirecionar automaticamente os endere\u00e7os HTTP para HTTPS.",
"encryption_https": "Porta HTTPS",
"encryption_https_desc": "Se a porta HTTPS estiver configurada, a interface administrativa do AdGuard Home ser\u00e1 acess\u00edvel via HTTPS e tamb\u00e9m fornecer\u00e1 o DNS-sobre-HTTPS no local '\/dns-query'.",
"encryption_dot": "Porta DNS-sobre-TLS",
"encryption_dot_desc": "Se essa porta estiver configurada, o AdGuard Home ir\u00e1 executar o servidor DNS-sobre- TSL nesta porta.",
"encryption_certificates": "Certificados",
"encryption_certificates_desc": "Para usar criptografia, voc\u00ea precisa fornecer uma cadeia de certificados SSL v\u00e1lida para seu dom\u00ednio. Voc\u00ea pode obter um certificado gratuito em <0> {{link}}<\/0> ou pode compr\u00e1-lo de uma das autoridades de certifica\u00e7\u00e3o confi\u00e1veis.",
"encryption_certificates_input": "Copie\/cole aqui seu certificado codificado em PEM.",
"encryption_status": "Status",
"encryption_expire": "Expira",
"encryption_key": "Chave privada",
"encryption_key_input": "Copie\/cole aqui a chave privada codificada em PEM para seu certificado.",
"encryption_enable": "Ativar criptografia (HTTPS, DNS-sobre-HTTPS e DNS-sobre-TLS)",
"encryption_enable_desc": "Se a criptografia estiver ativada, a interface administrativa do AdGuard Home funcionar\u00e1 em HTTPS, o servidor DNS ir\u00e1 capturar as solicita\u00e7\u00f5es por meio do DNS-sobre-HTTPS e DNS-sobre-TLS.",
"encryption_chain_valid": "Cadeia de chave v\u00e1lida.",
"encryption_chain_invalid": "A cadeia de certificado \u00e9 inv\u00e1lida",
"encryption_key_valid": "Esta \u00e9 uma chave privada {{type}} v\u00e1lida",
"encryption_key_invalid": "Esta \u00e9 uma chave privada {{type}} inv\u00e1lida",
"encryption_subject": "Assunto",
"encryption_issuer": "Emissor",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Voc\u00ea tem certeza de que deseja redefinir a configura\u00e7\u00e3o de criptografia?",
"topline_expiring_certificate": "Seu certificado SSL est\u00e1 prestes a expirar. Atualize suas <0>configura\u00e7\u00f5es de criptografia<\/]0>",
"topline_expired_certificate": "Seu certificado SSL est\u00e1 expirado. Atualize suas <0>configura\u00e7\u00f5es de criptografia<\/0>",
"form_error_port_range": "Digite um porta entre 80 e 65535",
"form_error_port_unsafe": "Esta porta n\u00e3o \u00e9 segura",
"form_error_equal": "N\u00e3o deve ser igual",
"form_error_password": "Senhas n\u00e3o coincidem",
"reset_settings": "Redefinir configura\u00e7\u00f5es",
"update_announcement": "AdGuard Home {{version}} est\u00e1 dispon\u00edvel!<0>Clique aqui<\/0> para mais informa\u00e7\u00f5es.",
"setup_guide": "Guia de configura\u00e7\u00e3o",
"dns_addresses": "Endere\u00e7os DNS"
}

View File

@@ -115,9 +115,9 @@
"example_comment_meaning": "\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439",
"example_comment_hash": "# \u0418 \u0432\u043e\u0442 \u0442\u0430\u043a \u0442\u043e\u0436\u0435",
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>",
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>",
"example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u0434\u043b\u044f <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u0438\u043b\u0438 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432",
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <0>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/0>",
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <0>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/0>",
"example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <0>DNS Stamps<\/0> \u0434\u043b\u044f <1>DNSCrypt<\/1> \u0438\u043b\u0438 <2>DNS-over-HTTPS<\/2> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432",
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",

View File

@@ -28,6 +28,7 @@
"dhcp_ip_addresses": "IP-adresser",
"dhcp_table_hostname": "V\u00e4rdnamn",
"dhcp_table_expires": "Utg\u00e5r",
"dhcp_warning": "Om du vill koppla in den inbyggda DHCP-servern m\u00e5ste du f\u00f6rs\u00e4kra dig om att det finns n\u00e5gra andra DHCP-servar som i s\u00e5 fall riskerar att orsaka avbrott p\u00e5 internet!",
"back": "Tiilbaka",
"dashboard": "Kontrollpanel",
"settings": "Inst\u00e4llningar",
@@ -115,10 +116,11 @@
"example_comment": "! H\u00e4r kommer en kommentar",
"example_comment_meaning": "Endast en kommentar",
"example_comment_hash": "# Ocks\u00e5 en kommentar",
"example_regex_meaning": "blockera \u00e5tkomst till en dom\u00e4n som matchar det angivna uttrycket",
"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_dot": "krypterat <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "krypterat <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "Du kan anv\u00e4nda <0>DNS-stamps<\/0> f\u00f6r <1>DNSCrypt<\/1> eller <2>DNS-\u00f6ver-HTTPS<\/2>\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",
@@ -156,5 +158,93 @@
"category_label": "Kategori",
"rule_label": "Regel",
"filter_label": "Filter",
"unknown_filter": "Ok\u00e4nt filter {{filterId}}"
"unknown_filter": "Ok\u00e4nt filter {{filterId}}",
"install_welcome_title": "V\u00e4lkommen till AdGuard Home!",
"install_welcome_desc": "AdGuard Home \u00e4r en DNS-server f\u00f6r n\u00e4tverkst\u00e4ckande annons- och sp\u00e5rningsblockering. Dess syfte \u00e4r att de dig kontroll \u00f6ver hela n\u00e4tverket och alla dina enheter, utan behov av att anv\u00e4nda klientbaserade program.",
"install_settings_title": "Administrat\u00f6rens webbgr\u00e4nssnitt",
"install_settings_listen": "\u00d6vervakningsgr\u00e4nssnitt",
"install_settings_port": "Port",
"install_settings_interface_link": "Din administrat\u00f6rssida f\u00f6r AdGuard Home finns p\u00e5 f\u00f6ljande adresser:",
"form_error_port": "Skriv in ett giltigt portnummer",
"install_settings_dns": "DNS-server",
"install_settings_dns_desc": "Du beh\u00f6ver st\u00e4lla in dina enheter eller din router f\u00f6r att anv\u00e4nda DNS-server p\u00e5 f\u00f6ljande adresser.",
"install_settings_all_interfaces": "Alla gr\u00e4nssnitt",
"install_auth_title": "Autentisering",
"install_auth_desc": "Det rekommenderas starkt att st\u00e4lla in l\u00f6senordsskydd till webbgr\u00e4nssnittets administrativa del i ditt AdGuard Home. \u00c4ven om den endast \u00e4r \u00e5tkomlig p\u00e5 ditt lokala n\u00e4tverk rekommenderas det \u00e4nd\u00e5 att skydda det mot o\u00f6nskad \u00e5tkomst.",
"install_auth_username": "Anv\u00e4ndarnamn",
"install_auth_password": "L\u00f6senord",
"install_auth_confirm": "Bekr\u00e4fta l\u00f6senord",
"install_auth_username_enter": "Skriv in anv\u00e4ndarnamn",
"install_auth_password_enter": "Skriv in l\u00f6senord",
"install_step": "Steg",
"install_devices_title": "St\u00e4ll in dina enheter",
"install_devices_desc": "F\u00f6r att kunna anv\u00e4nda AdGuard Home m\u00e5ste du s\u00e4lla in dina enheter f\u00f6r att utnyttja den.",
"install_submit_title": "Grattis!",
"install_submit_desc": "Inst\u00e4llningsproceduren \u00e4r klar och du kan b\u00f6rja anv\u00e4nda AdGuard Home.",
"install_devices_router": "Router",
"install_devices_router_desc": "Den h\u00e4r anpassningen kommer att automatiskt t\u00e4cka in alla de enheter som \u00e4r anslutna till din hemmarouter och du beh\u00f6ver d\u00e4rf\u00f6r inte konfigurera var och en individuellt.",
"install_devices_address": "AdGuard Home DNS-server t\u00e4cker f\u00f6ljande adresser",
"install_devices_router_list_1": "\u00d6ppna routern Inst\u00e4llningar. Vanligtvis f\u00e5r man \u00e5tkomst via en URL (http:\/\/192.168.0.1 eller 192.168.1.1)- Du kommer att bli ombes att ange ett l\u00f6senord. L\u00f6senordet kan st\u00e5 angivet p\u00e5 routerns bak- eller undersida. Om l\u00f6senordet \u00e4ndrats och du inte k\u00e4nner till det kan du \u00e5terst\u00e4lla med Reset-knappen. En del routrar kr\u00e4ver en s\u00e4rskild applikation som skall finnas p\u00e5 antingen din dator eller i din mobil.",
"install_devices_router_list_2": "Leta upp DHCP\/DNS-inst\u00e4llningarna. Titta efter DNS-tecken intill ett f\u00e4lt med tv\u00e5 eller tre upps\u00e4ttningar siffror, var och en uppdelade i grupper om fyra med en eller tre siffror.",
"install_devices_router_list_3": "Ange serveradressen till ditt AdGuard Home.",
"install_devices_windows_list_1": "\u00d6ppna Kontrollpanelen via Start eller Windows S\u00f6k.",
"install_devices_windows_list_2": "V\u00e4lj N\u00e4tverks och delningscenter, N\u00e4tverk och Internet.",
"install_devices_windows_list_3": "Leta upp \u00c4ndra n\u00e4tverkskortsalternativ",
"install_devices_windows_list_4": "Markera din aktiva anslutning. H\u00f6gerklicka p\u00e5 den och v\u00e4lj Egenskaper.",
"install_devices_windows_list_5": "Markera Internet Protocol Version 4 (TCP\/IP) och klicka p\u00e5 knappen Egenskaper.",
"install_devices_windows_list_6": "Markera Anv\u00e4nd f\u00f6ljande DNS-serveradresser och skriv in adresserna till ditt AdGuard Home.",
"install_devices_macos_list_1": "Klicka p\u00e5 Apple-ikonen och v\u00e4lj Systemalternativ.",
"install_devices_macos_list_2": "Klicka p\u00e5 N\u00e4tverk.",
"install_devices_macos_list_3": "V\u00e4lj den f\u00f6rsta anslutningen i listan och klicka p\u00e5 Avancerat.",
"install_devices_macos_list_4": "Klicka p\u00e5 DNS-fliken och skriv in AdGuard Homes serveradresser",
"install_devices_android_list_1": "V\u00e4lj Inst\u00e4llningar fr\u00e5n Androids hemknapp",
"install_devices_android_list_2": "Tryck p\u00e5 N\u00e4tverk och Internet, Wi-Fi. Alla tillg\u00e4ngliga n\u00e4tverk visas i en lista (det g\u00e5r inte all v\u00e4lja egen DNS p\u00e5 mobiln\u00e4tverk.",
"install_devices_android_list_3": "H\u00e5ll ner p\u00e5 n\u00e4tverksnamnet som du \u00e4r ansluten till och v\u00e4lj \u00c4ndra n\u00e4tverk.",
"install_devices_android_list_4": "P\u00e5 en del enheter kan du beh\u00f6va v\u00e4lja Avancerat f\u00f6r att komma \u00e5t ytterligare inst\u00e4llningar. F\u00f6r att \u00e4ndra p\u00e5 DNS-inst\u00e4llningar m\u00e5ste du byta IP-inst\u00e4llning fr\u00e5n DHCP till Statisk. P\u00e5 Android Pie v\u00e4ljs Privat DNS p\u00e5 N\u00e4tverk och internet.",
"install_devices_android_list_5": "\u00c4ndra DNS 1 och DNS 2 till serveradresserna f\u00f6r AdGuard Home.",
"install_devices_ios_list_1": "Tryck Inst\u00e4llningar fr\u00e5n hemsk\u00e4rmen.",
"install_devices_ios_list_2": "V\u00e4lj Wi_Fi p\u00e5 den v\u00e4nstra menyn (det g\u00e5r inte att st\u00e4lla in egen DNS f\u00f6r mobila n\u00e4tverk).",
"install_devices_ios_list_3": "Tryck p\u00e5 namnet p\u00e5 den aktiva anslutningen.",
"install_devices_ios_list_4": "Skriv in AdGuard Homes serveradresser i DNS-f\u00e4lten.",
"get_started": "Kom ig\u00e5ng",
"next": "N\u00e4sta",
"open_dashboard": "\u00d6ppna Kontrollbordet",
"install_saved": "Sparat utan fel",
"encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}<\/0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy\/paste your PEM-encoded certificates here.",
"encryption_status": "Status",
"encryption_expire": "Expires",
"encryption_key": "Private key",
"encryption_key_input": "Copy\/paste your PEM-encoded private key for your certificate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid",
"encryption_chain_invalid": "Certificate chain is invalid",
"encryption_key_valid": "This is a valid {{type}} private key",
"encryption_key_invalid": "This is an invalid {{type}} private key",
"encryption_subject": "Subject",
"encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings<\/0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings<\/0>.",
"form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal",
"form_error_password": "L\u00f6senorden \u00f6verensst\u00e4mmer inte",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info."
}

View File

@@ -113,9 +113,9 @@
"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-over-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_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ho\u1eb7c<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> ",
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <0>DNS Stamps<\/0> for <1>DNSCrypt<\/1> ho\u1eb7c <2>DNS-over-HTTPS<\/2> ",
"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",

View File

@@ -0,0 +1,253 @@
{
"upstream_parallel": "\u901a\u8fc7\u540c\u65f6\u67e5\u8be2\u6240\u6709\u4e0a\u6d41\u670d\u52a1\u5668\u4ee5\u4f7f\u7528\u5e76\u884c\u67e5\u8be2\u52a0\u901f\u89e3\u6790",
"bootstrap_dns": "Bootstrap DNS \u670d\u52a1\u5668",
"bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH\/DoT resolvers you specify as upstreams.",
"url_added_successfully": "\u7f51\u5740\u6dfb\u52a0\u6210\u529f",
"check_dhcp_servers": "\u68c0\u67e5 DHCP \u670d\u52a1\u5668",
"save_config": "\u4fdd\u5b58\u914d\u7f6e",
"enabled_dhcp": "DHCP \u670d\u52a1\u5668\u5df2\u542f\u7528",
"disabled_dhcp": "DHCP \u670d\u52a1\u5668\u5df2\u7981\u7528",
"dhcp_title": "DHCP \u670d\u52a1\u5668\uff08\u5b9e\u9a8c\u6027\uff09",
"dhcp_description": "\u5982\u679c\u4f60\u7684\u8def\u7531\u5668\u6ca1\u6709\u63d0\u4f9b\u52a8\u6001\u4e3b\u673a\u8bbe\u7f6e\u534f\u8bae\uff08DHCP\uff09\u8bbe\u7f6e\uff0c\u4f60\u53ef\u4ee5\u4f7f\u7528 AdGuard \u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\u3002",
"dhcp_enable": "\u542f\u7528 DHCP \u670d\u52a1\u5668",
"dhcp_disable": "\u7981\u7528 DHCP \u670d\u52a1\u5668",
"dhcp_not_found": "\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u672a\u68c0\u6d4b\u5230 DHCP \u670d\u52a1\u5668\u3002\u60a8\u53ef\u4ee5\u5b89\u5168\u5730\u542f\u7528\u5185\u7f6e DHCP \u670d\u52a1\u5668\u3002",
"dhcp_found": "\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u68c0\u6d4b\u5230 DHCP \u670d\u52a1\u5668\u3002\u5982\u679c\u542f\u7528\u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\u53ef\u80fd\u4e0d\u5b89\u5168\u3002",
"dhcp_leases": "DHCP \u79df\u7ea6",
"dhcp_leases_not_found": "\u672a\u68c0\u6d4b\u5230 DHCP \u79df\u7ea6",
"dhcp_config_saved": "\u4fdd\u5b58 DHCP \u670d\u52a1\u5668\u914d\u7f6e",
"form_error_required": "\u5fc5\u586b\u5b57\u6bb5",
"form_error_ip_format": "\u65e0\u6548\u7684 IPv4 \u683c\u5f0f",
"form_error_positive": "\u5fc5\u987b\u5927\u4e8e 0",
"dhcp_form_gateway_input": "\u7f51\u5173 IP",
"dhcp_form_subnet_input": "\u5b50\u7f51\u63a9\u7801",
"dhcp_form_range_title": "IP \u5730\u5740\u8303\u56f4",
"dhcp_form_range_start": "\u8d77\u59cb IP \u5730\u5740",
"dhcp_form_range_end": "\u672b\u5c3e IP \u5730\u5740",
"dhcp_form_lease_title": "DHCP \u79df\u7ea6\u65f6\u95f4\uff08\u79d2\uff09",
"dhcp_form_lease_input": "\u79df\u671f",
"dhcp_interface_select": "\u9009\u62e9 DHCP \u63a5\u53e3",
"dhcp_hardware_address": "\u786c\u4ef6\u5730\u5740",
"dhcp_ip_addresses": "IP \u5730\u5740",
"dhcp_table_hostname": "\u4e3b\u673a\u540d",
"dhcp_table_expires": "\u5230\u671f",
"dhcp_warning": "\u5982\u679c\u4f60\u60f3\u8981\u542f\u7528\u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\uff0c\u8bf7\u786e\u4fdd\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u6ca1\u6709\u5176\u5b83\u6d3b\u52a8\u7684 DHCP \u670d\u52a1\u5668\u3002\u5426\u5219\uff0c\u6b64\u64cd\u4f5c\u53ef\u80fd\u4f1a\u7834\u574f\u5df2\u8fde\u63a5\u8bbe\u5907\u7684\u7f51\u7edc\u8fde\u63a5\uff01",
"back": "\u8fd4\u56de",
"dashboard": "\u4eea\u8868\u76d8",
"settings": "\u8bbe\u7f6e",
"filters": "\u8fc7\u6ee4\u5668",
"query_log": "\u67e5\u8be2\u65e5\u5fd7",
"faq": "\u5e38\u89c1\u95ee\u9898",
"version": "\u7248\u672c",
"address": "\u5730\u5740",
"on": "\u542f\u7528\u4e2d",
"off": "\u7981\u7528\u4e2d",
"copyright": "\u7248\u6743",
"homepage": "\u4e3b\u9875",
"report_an_issue": "\u95ee\u9898\u53cd\u9988",
"enable_protection": "\u542f\u7528\u4fdd\u62a4",
"enabled_protection": "\u4fdd\u62a4\u5df2\u542f\u7528",
"disable_protection": "\u7981\u7528\u4fdd\u62a4",
"disabled_protection": "\u4fdd\u62a4\u5df2\u7981\u7528",
"refresh_statics": "\u5237\u65b0\u72b6\u6001",
"dns_query": "DNS\u67e5\u8be2",
"blocked_by": "\u5df2\u88ab\u8fc7\u6ee4\u5668\u62e6\u622a",
"stats_malware_phishing": "\u88ab\u62e6\u622a\u7684\u6076\u610f\/\u9493\u9c7c\u7f51\u7ad9",
"stats_adult": "\u88ab\u62e6\u622a\u7684\u6210\u4eba\u7f51\u7ad9",
"stats_query_domain": "\u8bf7\u6c42\u57df\u540d\u6392\u884c",
"for_last_24_hours": "\u5728\u8fc7\u53bb 24 \u5c0f\u65f6",
"no_domains_found": "\u672a\u627e\u5230\u57df\u540d",
"requests_count": "\u8bf7\u6c42\u6570",
"top_blocked_domains": "\u88ab\u62e6\u622a\u57df\u540d\u6392\u884c",
"top_clients": "\u5ba2\u6237\u7aef\u6392\u884c",
"no_clients_found": "\u672a\u627e\u5230\u5ba2\u6237\u7aef",
"general_statistics": "\u6982\u51b5\u7edf\u8ba1",
"number_of_dns_query_24_hours": "\u8fc7\u53bb 24 \u5c0f\u65f6\u5185\u5904\u7406\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours": "\u88ab\u5e7f\u544a\u8fc7\u6ee4\u5668\u548c Hosts \u62e6\u622a\u6e05\u5355\u62e6\u622a\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours_by_sec": "\u88ab AdGuard \u5b89\u5168\u6d4f\u89c8\u6a21\u5757\u62e6\u622a\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours_adult": "\u88ab\u62e6\u622a\u7684\u6210\u4eba\u7f51\u7ad9\u603b\u6570",
"enforced_save_search": "\u5f3a\u5236\u5b89\u5168\u641c\u7d22",
"number_of_dns_query_to_safe_search": "\u542f\u7528\u5f3a\u5236\u5b89\u5168\u641c\u7d22\u540e\u5bf9\u641c\u7d22\u5f15\u64ce\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"average_processing_time": "\u5e73\u5747\u5904\u7406\u65f6\u95f4",
"average_processing_time_hint": "\u5904\u7406 DNS \u8bf7\u6c42\u7684\u5e73\u5747\u65f6\u95f4\uff08\u6beb\u79d2\uff09",
"block_domain_use_filters_and_hosts": "\u4f7f\u7528\u8fc7\u6ee4\u5668\u548c Hosts \u6587\u4ef6\u4ee5\u62e6\u622a\u6307\u5b9a\u57df\u540d",
"filters_block_toggle_hint": "\u4f60\u53ef\u4ee5\u5728 <a href='#filters'>\u8fc7\u6ee4\u5668<\/a> \u8bbe\u7f6e\u4e2d\u6dfb\u52a0\u8fc7\u6ee4\u89c4\u5219\u3002",
"use_adguard_browsing_sec": "\u4f7f\u7528 AdGuard\u3010\u6d4f\u89c8\u5b89\u5168\u3011\u7f51\u9875\u670d\u52a1",
"use_adguard_browsing_sec_hint": "AdGuard Home \u5c06\u68c0\u67e5\u57df\u540d\u662f\u5426\u88ab\u6d4f\u89c8\u5b89\u5168\u670d\u52a1\u5217\u5165\u9ed1\u540d\u5355\u3002\u5b83\u5c06\u4f7f\u7528\u9690\u79c1\u6027\u5f3a\u7684\u68c0\u7d22 API \u6765\u6267\u884c\u68c0\u67e5\uff0c\u53ea\u6709\u57df\u540d\u7684 SHA256 \u7684\u77ed\u524d\u7f00\u4f1a\u88ab\u53d1\u9001\u5230\u670d\u52a1\u5668\u3002",
"use_adguard_parental": "\u4f7f\u7528 AdGuard \u3010\u5bb6\u957f\u63a7\u5236\u3011\u670d\u52a1",
"use_adguard_parental_hint": "AdGuard Home \u5c06\u4f7f\u7528\u4e0e\u6d4f\u89c8\u5b89\u5168\u670d\u52a1\u76f8\u540c\u7684\u9690\u79c1\u6027\u5f3a\u7684 API \u6765\u68c0\u67e5\u57df\u540d\u6307\u5411\u7684\u7f51\u7ad9\u662f\u5426\u5305\u542b\u6210\u4eba\u5185\u5bb9\u3002",
"enforce_safe_search": "\u5f3a\u5236\u5b89\u5168\u641c\u7d22",
"enforce_save_search_hint": "AdGuard Home \u5c06\u5bf9\u4ee5\u4e0b\u641c\u7d22\u5f15\u64ce\u5f3a\u5236\u542f\u7528\u5b89\u5168\u641c\u7d22\uff1aGoogle\u3001YouTube\u3001Bing \u548c Yandex\u3002",
"no_servers_specified": "\u672a\u627e\u5230\u6307\u5b9a\u7684\u670d\u52a1\u5668",
"no_settings": "\u672a\u627e\u5230\u8bbe\u7f6e",
"general_settings": "\u5e38\u89c4\u8bbe\u7f6e",
"upstream_dns": "\u4e0a\u6e38 DNS \u670d\u52a1\u5668",
"upstream_dns_hint": "\u5982\u679c\u6b64\u5904\u7559\u7a7a\uff0cAdGuard Home \u5c06\u4f1a\u4f7f\u7528 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u4f5c\u4e3a\u4e0a\u6e38 DNS\u3002\u5982\u679c\u60f3\u8981\u4f7f\u7528\u4f7f\u7528 DNS over TLS\uff0c\u8bf7\u4ee5 tls:\/\/ \u4e3a\u5f00\u5934\u3002",
"test_upstream_btn": "\u6d4b\u8bd5\u4e0a\u6e38 DNS",
"apply_btn": "\u5e94\u7528",
"disabled_filtering_toast": "\u8fc7\u6ee4\u5668\u5df2\u7981\u7528",
"enabled_filtering_toast": "\u8fc7\u6ee4\u5668\u5df2\u542f\u7528",
"disabled_safe_browsing_toast": "\u5b89\u5168\u6d4f\u89c8\u5df2\u7981\u7528",
"enabled_safe_browsing_toast": "\u5b89\u5168\u6d4f\u89c8\u5df2\u542f\u7528",
"disabled_parental_toast": "\u5bb6\u957f\u63a7\u5236\u5df2\u7981\u7528",
"enabled_parental_toast": "\u5bb6\u957f\u63a7\u5236\u5df2\u542f\u7528",
"disabled_safe_search_toast": "\u5b89\u5168\u641c\u7d22\u5df2\u7981\u7528",
"enabled_save_search_toast": "\u5b89\u5168\u641c\u7d22\u5df2\u542f\u7528",
"enabled_table_header": "\u5df2\u542f\u7528",
"name_table_header": "\u540d\u79f0",
"filter_url_table_header": "\u8fc7\u6ee4\u5668\u5730\u5740",
"rules_count_table_header": "\u89c4\u5219\u6570",
"last_time_updated_table_header": "\u4e0a\u6b21\u66f4\u65b0\u65f6\u95f4",
"actions_table_header": "\u6d3b\u8dc3\u72b6\u6001",
"delete_table_action": "\u5220\u9664",
"filters_and_hosts": "\u8fc7\u6ee4\u5668\u548c Hosts \u62e6\u622a\u6e05\u5355",
"filters_and_hosts_hint": "AdGuard Home \u53ef\u4ee5\u89e3\u6790\u57fa\u7840\u7684 adblock \u89c4\u5219\u548c Hosts \u8bed\u6cd5\u3002",
"no_filters_added": "\u672a\u6dfb\u52a0\u4efb\u4f55\u8fc7\u6ee4\u5668",
"add_filter_btn": "\u6dfb\u52a0\u8fc7\u6ee4\u5668",
"cancel_btn": "\u53d6\u6d88",
"enter_name_hint": "\u8f93\u5165\u540d\u79f0",
"enter_url_hint": "\u8f93\u5165 URL",
"check_updates_btn": "\u68c0\u67e5\u66f4\u65b0",
"new_filter_btn": "\u8ba2\u9605\u65b0\u7684\u8fc7\u6ee4\u5668",
"enter_valid_filter_url": "\u8f93\u5165\u4e00\u4e2a\u8fc7\u6ee4\u5668\u8ba2\u9605\u6216 Hosts \u6587\u4ef6\u7684\u6709\u6548 URL",
"custom_filter_rules": "\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u5668\u89c4\u5219",
"custom_filter_rules_hint": "\u8bf7\u786e\u4fdd\u6bcf\u884c\u53ea\u8f93\u5165\u4e00\u6761\u89c4\u5219\u3002\u4f60\u53ef\u4ee5\u8f93\u5165\u7b26\u5408 adblock \u8bed\u6cd5\u6216 Hosts \u8bed\u6cd5\u7684\u89c4\u5219\u3002",
"examples_title": "\u8303\u4f8b",
"example_meaning_filter_block": "\u62e6\u622a example.org \u57df\u540d\u53ca\u5176\u6240\u6709\u5b50\u57df\u540d",
"example_meaning_filter_whitelist": "\u653e\u884c example.org \u53ca\u5176\u6240\u6709\u5b50\u57df\u540d",
"example_meaning_host_block": "AdGuard Home \u73b0\u5728\u5c06\u4f1a\u628a example.org\uff08\u4f46\u4e0d\u5305\u62ec\u5b83\u7684\u5b50\u57df\u540d\uff09\u89e3\u6790\u5230 127.0.0.1\u3002",
"example_comment": "! \u8fd9\u662f\u4e00\u884c\u6ce8\u91ca",
"example_comment_meaning": "\u53ea\u662f\u4e00\u6761\u6ce8\u91ca",
"example_comment_hash": "# \u8fd9\u4e5f\u662f\u4e00\u884c\u6ce8\u91ca",
"example_regex_meaning": "\u963b\u6b62\u8bbf\u95ee\u4e0e\u6307\u5b9a\u7684\u6b63\u5219\u8868\u8fbe\u5f0f\u5339\u914d\u7684\u57df\u540d",
"example_upstream_regular": "\u5e38\u89c4 DNS\uff08\u57fa\u4e8e UDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u52a0\u5bc6 <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "\u4f60\u53ef\u4ee5\u4f7f\u7528 <0>DNSCrypt<\/0> \u7684 <1>DNS Stamps<\/1> \u6216\u8005 <2>DNS-over-HTTPS<\/2> \u89e3\u6790\u5668",
"example_upstream_tcp": "\u5e38\u89c4 DNS\uff08\u57fa\u4e8e TCP \uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u8fc7\u6ee4\u5668\u5df2\u66f4\u65b0\u81f3\u6700\u65b0",
"updated_upstream_dns_toast": "\u4e0a\u6e38 DNS \u5df2\u66f4\u65b0",
"dns_test_ok_toast": "\u6307\u5b9a\u7684 DNS \u670d\u52a1\u5668\u73b0\u5df2\u6b63\u5e38\u8fd0\u884c",
"dns_test_not_ok_toast": "\u670d\u52a1\u5668 \"{{key}}\"\uff1a\u65e0\u6cd5\u4f7f\u7528\uff0c\u8bf7\u68c0\u67e5\u4f60\u8f93\u5165\u7684\u662f\u5426\u6b63\u786e",
"unblock_btn": "\u653e\u884c",
"block_btn": "\u62e6\u622a",
"time_table_header": "\u65f6\u95f4",
"domain_name_table_header": "\u57df\u540d",
"type_table_header": "\u7c7b\u578b",
"response_table_header": "\u54cd\u5e94",
"client_table_header": "\u5ba2\u6237\u7aef",
"empty_response_status": "\u7a7a",
"show_all_filter_type": "\u663e\u793a\u6240\u6709",
"show_filtered_type": "\u663e\u793a\u88ab\u62e6\u622a\u7684",
"no_logs_found": "\u672a\u627e\u5230\u65e5\u5fd7",
"disabled_log_btn": "\u7981\u7528\u65e5\u5fd7",
"download_log_file_btn": "\u4e0b\u8f7d\u65e5\u5fd7\u6587\u4ef6",
"refresh_btn": "\u5237\u65b0",
"enabled_log_btn": "\u542f\u7528\u65e5\u5fd7",
"last_dns_queries": "\u6700\u8fd1\u7684 5000 \u4e2a DNS \u8bf7\u6c42",
"previous_btn": "\u4e0a\u4e00\u9875",
"next_btn": "\u4e0b\u4e00\u9875",
"loading_table_status": "\u52a0\u8f7d\u4e2d\u2026\u2026",
"page_table_footer_text": "\u9875",
"of_table_footer_text": "\u5728",
"rows_table_footer_text": "\u884c",
"updated_custom_filtering_toast": "\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5df2\u66f4\u65b0",
"rule_removed_from_custom_filtering_toast": "\u89c4\u5219\u5df2\u4ece\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5217\u8868\u4e2d\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u89c4\u5219\u5df2\u6dfb\u52a0\u5230\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5217\u8868\u4e2d",
"query_log_disabled_toast": "\u67e5\u8be2\u65e5\u5fd7\u5df2\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8be2\u65e5\u5fd7\u5df2\u542f\u7528",
"source_label": "\u6e90",
"found_in_known_domain_db": "\u6210\u529f\u5728\u5df2\u77e5\u57df\u540d\u6570\u636e\u5e93\u4e2d\u67e5\u8be2\u5230",
"category_label": "\u7c7b\u522b",
"rule_label": "\u89c4\u5219",
"filter_label": "\u8fc7\u6ee4\u5668",
"unknown_filter": "\u672a\u77e5\u8fc7\u6ee4\u5668 {{filterId}}",
"install_welcome_title": "\u6b22\u8fce\u4f7f\u7528 AdGuard Home\uff01",
"install_welcome_desc": "AdGuard Home \u662f\u4e00\u4e2a\u53ef\u5728\u7279\u5b9a\u7f51\u7edc\u8303\u56f4\u5185\u62e6\u622a\u6240\u6709\u5e7f\u544a\u548c\u8ddf\u8e2a\u5668\u7684 DNS \u670d\u52a1\u5668\u3002\u5b83\u7684\u76ee\u7684\u662f\u8ba9\u60a8\u63a7\u5236\u6574\u4e2a\u7f51\u7edc\u548c\u60a8\u7684\u6240\u6709\u8bbe\u5907\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u4efb\u4f55\u5ba2\u6237\u7aef\u7a0b\u5e8f\u3002",
"install_settings_title": "\u7f51\u9875\u7ba1\u7406\u754c\u9762",
"install_settings_listen": "\u76d1\u542c\u63a5\u53e3",
"install_settings_port": "\u7aef\u53e3",
"install_settings_interface_link": "\u60a8\u53ef\u4ee5\u901a\u8fc7\u4ee5\u4e0b\u5730\u5740\u8bbf\u95ee\u60a8\u7684 AdGuard Home \u7f51\u9875\u7ba1\u7406\u754c\u9762\uff1a",
"form_error_port": "\u8f93\u5165\u6709\u6548\u7684\u7aef\u53e3\u503c",
"install_settings_dns": "DNS \u670d\u52a1\u5668",
"install_settings_dns_desc": "\u60a8\u5c06\u9700\u8981\u4f7f\u7528\u4ee5\u4e0b\u5730\u5740\u6765\u8bbe\u7f6e\u60a8\u7684\u8bbe\u5907\u6216\u8def\u7531\u5668\u7684 DNS \u670d\u52a1\u5668\uff1a",
"install_settings_all_interfaces": "\u6240\u6709\u63a5\u53e3",
"install_auth_title": "\u8eab\u4efd\u8ba4\u8bc1",
"install_auth_desc": "\u6211\u4eec\u5f3a\u70c8\u5efa\u8bae\u60a8\u4e3a AdGuard Home \u7684\u7f51\u9875\u7ba1\u7406\u754c\u9762\u914d\u7f6e\u5bc6\u7801\u3002\u5c3d\u7ba1\u8be5\u9875\u9762\u53ea\u80fd\u901a\u8fc7\u60a8\u7684\u672c\u5730\u7f51\u7edc\u8bbf\u95ee\uff0c\u4f46\u907f\u514d\u5b83\u88ab\u4e0d\u52a0\u9650\u5236\u5730\u8bbf\u95ee\u4ecd\u5341\u5206\u91cd\u8981\u3002",
"install_auth_username": "\u7528\u6237\u540d",
"install_auth_password": "\u5bc6\u7801",
"install_auth_confirm": "\u786e\u8ba4\u5bc6\u7801",
"install_auth_username_enter": "\u8f93\u5165\u7528\u6237\u540d",
"install_auth_password_enter": "\u8f93\u5165\u5bc6\u7801",
"install_step": "\u6b65\u9aa4",
"install_devices_title": "\u914d\u7f6e\u60a8\u7684\u8bbe\u5907",
"install_devices_desc": "\u4e3a\u4fdd\u8bc1 AdGuard Home \u53ef\u4ee5\u5f00\u59cb\u6b63\u5e38\u5de5\u4f5c\uff0c\u60a8\u9700\u8981\u5728\u8bbe\u5907\u4e0a\u5bf9\u5176\u8fdb\u884c\u914d\u7f6e\u3002",
"install_submit_title": "\u606d\u559c\u60a8\uff01",
"install_submit_desc": "\u5b89\u88c5\u8fc7\u7a0b\u5df2\u7ecf\u5b8c\u6210\uff0c\u60a8\u53ef\u4ee5\u5f00\u59cb\u4f7f\u7528 AdGuard Home \u4e86\u3002",
"install_devices_router": "\u8def\u7531\u5668",
"install_devices_router_desc": "\u6b64\u8bbe\u7f6e\u5c06\u81ea\u52a8\u8986\u76d6\u8fde\u63a5\u5230\u60a8\u7684\u5bb6\u5ead\u8def\u7531\u5668\u7684\u6240\u6709\u8bbe\u5907\uff0c\u60a8\u4e0d\u9700\u8981\u624b\u52a8\u914d\u7f6e\u5b83\u4eec\u3002",
"install_devices_address": "AdGuard Home DNS \u670d\u52a1\u5668\u6b63\u5728\u76d1\u542c\u4ee5\u4e0b\u5730\u5740",
"install_devices_router_list_1": "\u6253\u5f00\u60a8\u7684\u8def\u7531\u5668\u914d\u7f6e\u754c\u9762\u3002\u901a\u5e38\u60c5\u51b5\u4e0b\uff0c\u60a8\u53ef\u4ee5\u901a\u8fc7\u6d4f\u89c8\u5668\u8bbf\u95ee\u5730\u5740\uff08\u5982 http:\/\/192.168.0.1\/ \u6216 http:\/\/192.168.1.1 \uff09\u3002\u6253\u5f00\u540e\u60a8\u53ef\u80fd\u9700\u8981\u8f93\u5165\u5bc6\u7801\u4ee5\u8fdb\u5165\u914d\u7f6e\u754c\u9762\u3002\u5982\u679c\u60a8\u4e0d\u8bb0\u5f97\u5bc6\u7801\uff0c\u901a\u5e38\u53ef\u4ee5\u901a\u8fc7\u6309\u4e0b\u8def\u7531\u5668\u4e0a\u7684\u91cd\u7f6e\u6309\u94ae\u6765\u91cd\u8bbe\u5bc6\u7801\u3002\u4e00\u4e9b\u8def\u7531\u5668\u53ef\u80fd\u9700\u8981\u901a\u8fc7\u7279\u5b9a\u7684\u5e94\u7528\u6765\u8fdb\u884c\u8fd9\u4e00\u64cd\u4f5c\uff0c\u8bf7\u786e\u4fdd\u60a8\u5df2\u7ecf\u5728\u8ba1\u7b97\u673a\u6216\u624b\u673a\u4e0a\u5b89\u88c5\u4e86\u76f8\u5173\u5e94\u7528\u3002",
"install_devices_router_list_2": "\u627e\u5230\u8def\u7531\u5668\u7684 DHCP\/DNS \u8bbe\u7f6e\u9875\u9762\u3002\u60a8\u4f1a\u5728 DNS \u8fd9\u4e00\u5355\u8bcd\u65c1\u8fb9\u627e\u5230\u4e24\u5230\u4e09\u884c\u5141\u8bb8\u8f93\u5165\u7684\u8f93\u5165\u6846\uff0c\u6bcf\u4e00\u884c\u8f93\u5165\u6846\u5206\u4e3a\u56db\u7ec4\uff0c\u6bcf\u7ec4\u5141\u8bb8\u8f93\u5165\u4e00\u5230\u4e09\u4e2a\u6570\u5b57\u3002",
"install_devices_router_list_3": "\u8bf7\u5728\u6b64\u5904\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_windows_list_1": "\u901a\u8fc7\u5f00\u59cb\u83dc\u5355\u6216 Windows \u641c\u7d22\u529f\u80fd\u6253\u5f00\u63a7\u5236\u9762\u677f\u3002",
"install_devices_windows_list_2": "\u70b9\u51fb\u8fdb\u5165 \u201d\u7f51\u7edc\u548c Internet\u201c \u540e\uff0c\u518d\u6b21\u70b9\u51fb\u8fdb\u5165 \u201c\u7f51\u7edc\u548c\u5171\u4eab\u4e2d\u5fc3\u201d",
"install_devices_windows_list_3": "\u5728\u7a97\u53e3\u7684\u5de6\u4fa7\u627e\u5230 \u201d\u66f4\u6539\u9002\u914d\u5668\u8bbe\u7f6e\u201c \u5e76\u70b9\u51fb\u8fdb\u5165\u3002",
"install_devices_windows_list_4": "\u9009\u62e9\u60a8\u6b63\u5728\u8fde\u63a5\u7684\u7f51\u7edc\u8bbe\u5907\uff0c\u53f3\u51fb\u5b83\u5e76\u9009\u62e9 \u201d\u5c5e\u6027\u201c \u3002",
"install_devices_windows_list_5": "\u5728\u5217\u8868\u4e2d\u627e\u5230 \u201dInternet \u534f\u8bae\u7248\u672c 4 (TCP\/IPv4)\u201c \uff0c\u9009\u62e9\u5e76\u518d\u6b21\u70b9\u51fb \u201d\u5c5e\u6027\u201c \u3002",
"install_devices_windows_list_6": "\u9009\u62e9 \u201d\u4f7f\u7528\u4e0b\u9762\u7684 DNS \u670d\u52a1\u5668\u5730\u5740\u201c \uff0c\u5e76\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_macos_list_1": "\u70b9\u51fb\u82f9\u679c\u56fe\u6807\uff0c\u8fdb\u5165 \u201d\u7cfb\u7edf\u9996\u9009\u9879\u201c\u3002",
"install_devices_macos_list_2": "\u70b9\u51fb \u201d\u7f51\u7edc\u201c \u3002",
"install_devices_macos_list_3": "\u9009\u62e9\u5728\u5217\u8868\u4e2d\u7684\u7b2c\u4e00\u4e2a\u8fde\u63a5\uff0c\u5e76\u70b9\u51fb \u201d\u9ad8\u7ea7\u201c \u3002",
"install_devices_macos_list_4": "\u9009\u62e9 \u201dDNS\u201c \u9009\u9879\u5361\uff0c\u5e76\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_android_list_1": "\u5728\u5b89\u5353\u4e3b\u5c4f\u5e55\u83dc\u5355\u4e2d\u70b9\u51fb\u8bbe\u7f6e\u3002",
"install_devices_android_list_2": "\u70b9\u51fb\u83dc\u5355\u4e0a\u7684 \u201d\u65e0\u7ebf\u5c40\u57df\u7f51\u201c \u9009\u9879\u3002\u5728\u5c4f\u5e55\u4e0a\u5c06\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7f51\u7edc\uff08\u8702\u7a9d\u79fb\u52a8\u7f51\u7edc\u4e0d\u652f\u6301\u4fee\u6539 DNS \uff09\u3002",
"install_devices_android_list_3": "\u957f\u6309\u5f53\u524d\u5df2\u8fde\u63a5\u7684\u7f51\u7edc\uff0c\u7136\u540e\u70b9\u51fb \u201d\u4fee\u6539\u7f51\u7edc\u8bbe\u7f6e\u201c \u3002",
"install_devices_android_list_4": "\u5728\u67d0\u4e9b\u8bbe\u5907\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u9009\u4e2d \u201d\u9ad8\u7ea7\u201c \u590d\u9009\u6846\u4ee5\u67e5\u770b\u8fdb\u4e00\u6b65\u7684\u8bbe\u7f6e\u3002\u60a8\u53ef\u80fd\u9700\u8981\u8c03\u6574\u60a8\u5b89\u5353\u8bbe\u5907\u7684 DNS \u8bbe\u7f6e\uff0c\u6216\u662f\u9700\u8981\u5c06 IP \u8bbe\u7f6e\u4ece DHCP \u5207\u6362\u5230\u9759\u6001\u3002",
"install_devices_android_list_5": "\u5c06 \"DNS 1 \/ \u4e3b DNS\" \u548c \u201dDNS 2 \/ \u526f DNS\u201c \u7684\u503c\u6539\u4e3a\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_ios_list_1": "\u4ece\u4e3b\u5c4f\u5e55\u4e2d\u70b9\u51fb \u201d\u8bbe\u7f6e\u201c \u3002",
"install_devices_ios_list_2": "\u4ece\u5de6\u4fa7\u76ee\u5f55\u4e2d\u9009\u62e9 \u201d\u65e0\u7ebf\u5c40\u57df\u7f51\u201c\uff08\u79fb\u52a8\u6570\u636e\u7f51\u7edc\u73af\u5883\u4e0b\u4e0d\u652f\u6301\u4fee\u6539 DNS \uff09\u3002",
"install_devices_ios_list_3": "\u70b9\u51fb\u5f53\u524d\u5df2\u8fde\u63a5\u7f51\u7edc\u7684\u540d\u79f0\u3002",
"install_devices_ios_list_4": "\u5728 DNS \u5b57\u6bb5\u4e2d\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"get_started": "\u5f00\u59cb\u914d\u7f6e",
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u6253\u5f00\u4eea\u8868\u76d8",
"install_saved": "\u4fdd\u5b58\u6210\u529f",
"encryption_title": "\u52a0\u5bc6",
"encryption_desc": "\u4e3a DNS \u4e0e\u7f51\u9875\u7ba1\u7406\u754c\u9762\u542f\u7528\u52a0\u5bc6\uff08HTTPS\/TLS\uff09",
"encryption_config_saved": "\u52a0\u5bc6\u914d\u7f6e\u5df2\u4fdd\u5b58",
"encryption_server": "\u670d\u52a1\u5668\u540d\u79f0",
"encryption_server_enter": "\u8f93\u5165\u60a8\u7684\u57df\u540d",
"encryption_server_desc": "\u82e5\u8981\u4f7f\u7528 HTTPS \uff0c\u60a8\u9700\u8981\u8f93\u5165\u4e0e SSL \u8bc1\u4e66\u76f8\u5339\u914d\u7684\u670d\u52a1\u5668\u540d\u79f0\u3002",
"encryption_redirect": "HTTPS \u81ea\u52a8\u91cd\u5b9a\u5411",
"encryption_redirect_desc": "\u5982\u679c\u52fe\u9009\u6b64\u9009\u9879\uff0cAdGuard Home \u5c06\u81ea\u52a8\u5c06\u60a8\u4ece HTTP \u91cd\u5b9a\u5411\u5230 HTTPS \u5730\u5740\u3002",
"encryption_https": "HTTPS \u7aef\u53e3",
"encryption_https_desc": "\u5982\u679c\u914d\u7f6e\u4e86 HTTPS \u7aef\u53e3\uff0cAdGuard Home \u7ba1\u7406\u754c\u9762\u5c06\u53ef\u4ee5\u901a\u8fc7 HTTPS \u8bbf\u95ee\uff0c\u5b83\u8fd8\u5c06\u5728\u5728 '\/dns-query' \u4f4d\u7f6e\u63d0\u4f9b DNS-over-HTTPS \u3002",
"encryption_dot": "DNS-over-TLS \u7aef\u53e3",
"encryption_dot_desc": "\u5982\u679c\u914d\u7f6e\u4e86\u6b64\u7aef\u53e3\uff0cAdGuard Home \u5c06\u5728\u6b64\u7aef\u53e3\u4e0a\u8fd0\u884c\u4e00\u4e2a DNS-over-TLS \u670d\u52a1\u5668\u3002",
"encryption_certificates": "\u8bc1\u4e66",
"encryption_certificates_desc": "\u4e3a\u4e86\u4f7f\u7528\u52a0\u5bc6\uff0c\u60a8\u9700\u8981\u4e3a\u57df\u63d0\u4f9b\u6709\u6548\u7684 SSL \u8bc1\u4e66\u94fe\u3002\u60a8\u53ef\u4ee5\u5728 <0>{{link}}<\/0> \u4e0a\u83b7\u5f97\u514d\u8d39\u8bc1\u4e66\uff0c\u4e5f\u53ef\u4ee5\u4ece\u53d7\u4fe1\u4efb\u7684\u8bc1\u4e66\u9881\u53d1\u673a\u6784\u8d2d\u4e70\u8bc1\u4e66\u3002",
"encryption_certificates_input": "\u5c06\u60a8\u4ee5 PEM \u683c\u5f0f\u7f16\u7801\u7684\u8bc1\u4e66\u590d\u5236\u7c98\u8d34\u5230\u6b64\u5904\u3002",
"encryption_status": "\u72b6\u6001",
"encryption_expire": "\u6709\u6548\u671f",
"encryption_key": "\u79c1\u94a5",
"encryption_key_input": "\u5c06\u60a8\u4ee5 PEM \u683c\u5f0f\u7f16\u7801\u7684\u8bc1\u4e66\u79c1\u94a5\u590d\u5236\u7c98\u8d34\u5230\u6b64\u5904\u3002",
"encryption_enable": "\u542f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS\u3001DNS-over-TLS\uff09",
"encryption_enable_desc": "\u5982\u679c\u542f\u7528\u52a0\u5bc6\u9009\u9879\uff0cAdGuard Home \u7684\u7f51\u9875\u7ba1\u7406\u754c\u9762\u5c06\u901a\u8fc7 HTTPS \u8fde\u63a5\u8bbf\u95ee\uff0c\u540c\u65f6 DNS \u670d\u52a1\u5668\u5c06\u76d1\u542c\u901a\u8fc7 DNS-over-HTTPS \u4e0e DNS-over-TLS \u53d1\u9001\u7684\u8bf7\u6c42\u3002",
"encryption_chain_valid": "\u8bc1\u4e66\u94fe\u9a8c\u8bc1\u6709\u6548",
"encryption_chain_invalid": "\u8bc1\u4e66\u94fe\u9a8c\u8bc1\u65e0\u6548",
"encryption_key_valid": "\u8be5 {{type}} \u79c1\u94a5\u9a8c\u8bc1\u6709\u6548",
"encryption_key_invalid": "\u8be5 {{type}} \u79c1\u94a5\u9a8c\u8bc1\u65e0\u6548",
"encryption_subject": "\u4f7f\u7528\u8005",
"encryption_issuer": "\u9881\u53d1\u8005",
"encryption_hostnames": "\u4e3b\u673a\u540d",
"encryption_reset": "\u60a8\u786e\u5b9a\u60f3\u8981\u91cd\u7f6e\u52a0\u5bc6\u8bbe\u7f6e\uff1f",
"topline_expiring_certificate": "\u60a8\u7684 SSL \u8bc1\u4e66\u5373\u5c06\u8fc7\u671f\u3002\u8bf7\u66f4\u65b0 <0>\u52a0\u5bc6\u8bbe\u7f6e<\/0> \u3002",
"topline_expired_certificate": "\u60a8\u7684 SSL \u8bc1\u4e66\u5df2\u8fc7\u671f\u3002\u8bf7\u66f4\u65b0 <0>\u52a0\u5bc6\u8bbe\u7f6e<\/0> \u3002",
"form_error_port_range": "\u8f93\u5165 80 - 65535 \u8303\u56f4\u5185\u7684\u7aef\u53e3\u503c",
"form_error_port_unsafe": "\u8fd9\u662f\u4e00\u4e2a\u4e0d\u5b89\u5168\u7684\u7aef\u53e3",
"form_error_equal": "\u4e0d\u5e94\u8be5\u76f8\u540c",
"form_error_password": "\u5bc6\u7801\u4e0d\u5339\u914d",
"reset_settings": "\u91cd\u7f6e\u8bbe\u7f6e",
"update_announcement": "AdGuard Home {{version}} \u73b0\u5df2\u53d1\u5e03\uff01 <0>\u70b9\u51fb\u6b64\u5904<\/0> \u4ee5\u83b7\u53d6\u8be6\u7ec6\u4fe1\u606f\u3002"
}

View File

@@ -1,4 +1,8 @@
{
"example_upstream_reserved": "\u60a8\u53ef\u660e\u78ba\u6307\u5b9a<0>\u7528\u65bc\u7279\u5b9a\u7684\u7db2\u57df<\/0>\u4e4bDNS\u4e0a\u6e38",
"upstream_parallel": "\u900f\u904e\u540c\u6642\u5730\u67e5\u8a62\u6240\u6709\u4e0a\u6e38\u7684\u4f3a\u670d\u5668\uff0c\u4f7f\u7528\u4e26\u884c\u7684\u67e5\u8a62\u4ee5\u52a0\u901f\u89e3\u6790",
"bootstrap_dns": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS \u4f3a\u670d\u5668",
"bootstrap_dns_desc": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS\u4f3a\u670d\u5668\u88ab\u7528\u65bc\u89e3\u6790\u60a8\u660e\u78ba\u6307\u5b9a\u4f5c\u70ba\u4e0a\u6e38\u7684DoH\/DoT\u89e3\u6790\u5668\u4e4bIP\u4f4d\u5740\u3002",
"url_added_successfully": "\u7db2\u5740\u88ab\u6210\u529f\u5730\u52a0\u5165",
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"save_config": "\u5132\u5b58\u914d\u7f6e",
@@ -79,7 +83,7 @@
"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",
"upstream_dns_hint": "\u5982\u679c\u60a8\u5c07\u8a72\u6b04\u4f4d\u7559\u7a7a\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002",
"test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41",
"apply_btn": "\u5957\u7528",
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
@@ -116,11 +120,11 @@
"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_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u914d\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u7b26\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
"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\u95dc\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_dot": "\u52a0\u5bc6\u7684 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u52a0\u5bc6\u7684 <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <1>DNSCrypt<\/1> \u6216 <2>DNS-over-HTTPS<\/2> \u89e3\u6790\u5668\u4e4b <0>DNS \u6233\u8a18<\/0>",
"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",
@@ -210,5 +214,43 @@
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc"
"encryption_title": "\u52a0\u5bc6",
"encryption_desc": "\u52a0\u5bc6\uff08HTTPS\/TLS\uff09\u652f\u63f4\u4f9bDNS\u548c\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5169\u8005",
"encryption_config_saved": "\u52a0\u5bc6\u914d\u7f6e\u5df2\u88ab\u5132\u5b58",
"encryption_server": "\u4f3a\u670d\u5668\u540d\u7a31",
"encryption_server_enter": "\u8f38\u5165\u60a8\u7684\u57df\u540d",
"encryption_server_desc": "\u70ba\u4e86\u4f7f\u7528HTTPS\uff0c\u60a8\u9700\u8981\u8f38\u5165\u8207\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u76f8\u7b26\u7684\u4f3a\u670d\u5668\u540d\u7a31\u3002",
"encryption_redirect": "\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u5230HTTPS",
"encryption_redirect_desc": "\u5982\u679c\u88ab\u52fe\u9078\uff0cAdGuard Home\u5c07\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u60a8\u5f9eHTTP\u5230HTTPS\u4f4d\u5740\u3002",
"encryption_https": "HTTPS \u9023\u63a5\u57e0",
"encryption_https_desc": "\u5982\u679cHTTPS\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u4e14\u5b83\u4e5f\u5c07\u65bc '\/dns-query' \u4f4d\u7f6e\u4e0a\u63d0\u4f9bDNS-over-HTTPS\u3002",
"encryption_dot": "DNS-over-TLS \u9023\u63a5\u57e0",
"encryption_dot_desc": "\u5982\u679c\u8a72\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u5c07\u65bc\u6b64\u9023\u63a5\u57e0\u4e0a\u904b\u884cDNS-over-TLS\u4f3a\u670d\u5668\u3002",
"encryption_certificates": "\u6191\u8b49",
"encryption_certificates_desc": "\u70ba\u4e86\u4f7f\u7528\u52a0\u5bc6\uff0c\u60a8\u9700\u8981\u63d0\u4f9b\u6709\u6548\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u93c8\u7d50\u4f9b\u60a8\u7684\u7db2\u57df\u3002\u65bc <0>{{link}}<\/0> \u4e0a\u60a8\u53ef\u53d6\u5f97\u514d\u8cbb\u7684\u6191\u8b49\u6216\u60a8\u53ef\u5f9e\u53d7\u4fe1\u4efb\u7684\u6191\u8b49\u6388\u6b0a\u55ae\u4f4d\u4e4b\u4e00\u8cfc\u8cb7\u5b83\u3002",
"encryption_certificates_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u6191\u8b49\u3002",
"encryption_status": "\u72c0\u614b",
"encryption_expire": "\u5230\u671f",
"encryption_key": "\u79c1\u5bc6\u91d1\u9470",
"encryption_key_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u79c1\u5bc6\u91d1\u9470\u4f9b\u60a8\u7684\u6191\u8b49\u3002",
"encryption_enable": "\u555f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS\u548cDNS-over-TLS\uff09",
"encryption_enable_desc": "\u5982\u679c\u52a0\u5bc6\u88ab\u555f\u7528\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u904b\u4f5c\uff0c\u4e14\u8a72DNS\u4f3a\u670d\u5668\u5c07\u7559\u5fc3\u76e3\u807d\u900f\u904eDNS-over-HTTPS\u548cDNS-over-TLS\u4e4b\u8acb\u6c42\u3002",
"encryption_chain_valid": "\u6191\u8b49\u93c8\u7d50\u70ba\u6709\u6548\u7684",
"encryption_chain_invalid": "\u6191\u8b49\u93c8\u7d50\u70ba\u7121\u6548\u7684",
"encryption_key_valid": "\u6b64\u70ba\u6709\u6548\u7684 {{type}} \u79c1\u5bc6\u91d1\u9470",
"encryption_key_invalid": "\u6b64\u70ba\u7121\u6548\u7684 {{type}} \u79c1\u5bc6\u91d1\u9470",
"encryption_subject": "\u7269\u4ef6",
"encryption_issuer": "\u7c3d\u767c\u8005",
"encryption_hostnames": "\u4e3b\u6a5f\u540d\u7a31",
"encryption_reset": "\u60a8\u78ba\u5b9a\u60a8\u60f3\u8981\u91cd\u7f6e\u52a0\u5bc6\u8a2d\u5b9a\u55ce\uff1f",
"topline_expiring_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u5373\u5c07\u5230\u671f\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
"topline_expired_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u70ba\u5df2\u5230\u671f\u7684\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
"form_error_port_range": "\u572880-65535\u4e4b\u7bc4\u570d\u5167\u8f38\u5165\u9023\u63a5\u57e0\u503c",
"form_error_port_unsafe": "\u6b64\u70ba\u4e0d\u5b89\u5168\u7684\u9023\u63a5\u57e0",
"form_error_equal": "\u4e0d\u61c9\u70ba\u76f8\u7b49\u7684",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc",
"reset_settings": "\u91cd\u7f6e\u8a2d\u5b9a",
"update_announcement": "AdGuard Home {{version}} \u73fe\u70ba\u53ef\u7528\u7684\uff01\u95dc\u65bc\u66f4\u591a\u7684\u8cc7\u8a0a\uff0c<0>\u9ede\u64ca\u9019\u88e1<\/0>\u3002",
"setup_guide": "\u5b89\u88dd\u6307\u5357",
"dns_addresses": "DNS \u4f4d\u5740"
}

View File

@@ -3,7 +3,7 @@ import round from 'lodash/round';
import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
import { SETTINGS_NAMES } from '../helpers/constants';
import Api from '../api/Api';
@@ -139,6 +139,36 @@ export const toggleProtection = status => async (dispatch) => {
}
};
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = () => async (dispatch) => {
dispatch(getVersionRequest());
try {
const newVersion = await apiClient.getGlobalVersion();
dispatch(getVersionSuccess(newVersion));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getVersionFailure());
}
};
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
export const getClients = () => async (dispatch) => {
dispatch(getClientsRequest());
try {
const clients = await apiClient.getGlobalClients();
dispatch(getClientsSuccess(clients));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getClientsFailure());
}
};
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
@@ -148,6 +178,8 @@ export const getDnsStatus = () => async (dispatch) => {
try {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus));
dispatch(getVersion());
dispatch(getClients());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
@@ -205,21 +237,6 @@ export const getStats = () => async (dispatch) => {
}
};
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = () => async (dispatch) => {
dispatch(getVersionRequest());
try {
const newVersion = await apiClient.getGlobalVersion();
dispatch(getVersionSuccess(newVersion));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getVersionFailure());
}
};
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
@@ -452,10 +469,18 @@ export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
export const setUpstream = url => async (dispatch) => {
export const setUpstream = config => async (dispatch) => {
dispatch(setUpstreamRequest());
try {
await apiClient.setUpstream(url);
const values = { ...config };
values.bootstrap_dns = (
values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns)
) || [];
values.upstream_dns = (
values.upstream_dns && normalizeTextarea(values.upstream_dns)
) || [];
await apiClient.setUpstream(values);
dispatch(addSuccessToast('updated_upstream_dns_toast'));
dispatch(setUpstreamSuccess());
} catch (error) {
@@ -468,11 +493,18 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = servers => async (dispatch) => {
export const testUpstream = config => async (dispatch) => {
dispatch(testUpstreamRequest());
try {
const upstreamResponse = await apiClient.testUpstream(servers);
const values = { ...config };
values.bootstrap_dns = (
values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns)
) || [];
values.upstream_dns = (
values.upstream_dns && normalizeTextarea(values.upstream_dns)
) || [];
const upstreamResponse = await apiClient.testUpstream(values);
const testMessages = Object.keys(upstreamResponse).map((key) => {
const message = upstreamResponse[key];
if (message !== 'OK') {

View File

@@ -44,3 +44,18 @@ export const setAllSettings = values => async (dispatch) => {
dispatch(prevStep());
}
};
export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');
export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');
export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');
export const checkConfig = values => async (dispatch) => {
dispatch(checkConfigRequest());
try {
const check = await apiClient.checkConfig(values);
dispatch(checkConfigSuccess(check));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(checkConfigFailure());
}
};

View File

@@ -34,11 +34,12 @@ export default class Api {
GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' };
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
restartGlobalFiltering() {
const { path, method } = this.GLOBAL_RESTART;
@@ -110,7 +111,7 @@ export default class Api {
const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS;
const config = {
data: url,
header: { 'Content-Type': 'text/plain' },
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -119,7 +120,7 @@ export default class Api {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {
data: servers,
header: { 'Content-Type': 'text/plain' },
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -139,13 +140,18 @@ export default class Api {
return this.makeRequest(path, method);
}
getGlobalClients() {
const { path, method } = this.GLOBAL_CLIENTS;
return this.makeRequest(path, method);
}
// Filtering
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' };
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'PUT' };
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'DELETE' };
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'PUT' };
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' };
FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' };
FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
@@ -344,6 +350,7 @@ export default class Api {
// Installation
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
getDefaultAddresses() {
const { path, method } = this.INSTALL_GET_ADDRESSES;
@@ -359,6 +366,15 @@ export default class Api {
return this.makeRequest(path, method, parameters);
}
checkConfig(config) {
const { path, method } = this.INSTALL_CHECK_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
// DNS-over-HTTPS and DNS-over-TLS
TLS_STATUS = { path: 'tls/status', method: 'GET' };
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };

View File

@@ -14,8 +14,9 @@ import Dashboard from '../../containers/Dashboard';
import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import SetupGuide from '../../containers/SetupGuide';
import Toasts from '../Toasts';
import Footer from '../ui/Footer';
import Status from '../ui/Status';
import UpdateTopline from '../ui/UpdateTopline';
import EncryptionTopline from '../ui/EncryptionTopline';
@@ -24,7 +25,6 @@ import i18n from '../../i18n';
class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
this.props.getVersion();
}
componentDidUpdate(prevProps) {
@@ -86,6 +86,7 @@ class App extends Component {
<Route path="/settings" component={Settings} />
<Route path="/filters" component={Filters} />
<Route path="/logs" component={Logs} />
<Route path="/guide" component={SetupGuide} />
</Fragment>
}
</div>
@@ -103,7 +104,6 @@ App.propTypes = {
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
};

View File

@@ -7,7 +7,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import { getPercent } from '../../helpers/helpers';
import { getPercent, getClientName } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Clients extends Component {
@@ -23,7 +23,24 @@ class Clients extends Component {
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
Cell: ({ value }) => {
const clientName = getClientName(this.props.clients, value);
let client;
if (clientName) {
client = <span>{clientName} <small>({value})</small></span>;
} else {
client = value;
}
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{client}
</span>
</div>
);
},
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
}, {
Header: <Trans>requests_count</Trans>,
@@ -61,6 +78,7 @@ Clients.propTypes = {
topClients: PropTypes.object.isRequired,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
clients: PropTypes.array.isRequired,
t: PropTypes.func,
};

View File

@@ -46,6 +46,7 @@ class Dashboard extends Component {
dashboard.processing ||
dashboard.processingStats ||
dashboard.processingStatsHistory ||
dashboard.processingClients ||
dashboard.processingTopStats;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
@@ -94,6 +95,7 @@ class Dashboard extends Component {
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
clients={dashboard.clients}
/>
</div>
<div className="col-lg-6">

View File

@@ -76,6 +76,13 @@
font-weight: 600;
}
.nav-version__link {
position: relative;
display: inline-block;
border-bottom: 1px dashed #495057;
cursor: pointer;
}
.header-brand-img {
height: 32px;
}
@@ -110,6 +117,10 @@
.nav-version {
padding: 0;
}
.nav-icon {
display: none;
}
}
@media screen and (min-width: 1280px) {
@@ -120,6 +131,10 @@
.nav-version {
font-size: 0.85rem;
}
.nav-icon {
display: block;
}
}
.dns-status {

View File

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

View File

@@ -2,24 +2,35 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import { getDnsAddress } from '../../helpers/helpers';
function Version(props) {
const { dnsVersion, dnsAddress, dnsPort } = props;
const { dnsVersion, dnsAddresses, dnsPort } = props;
return (
<div className="nav-version">
<div className="nav-version__text">
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span>
</div>
<div className="nav-version__text">
<Trans>address</Trans>: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
<div className="nav-version__link">
<div className="popover__trigger popover__trigger--address">
<Trans>dns_addresses</Trans>
</div>
<div className="popover__body popover__body--address">
<div className="popover__list">
{dnsAddresses
.map(ip => <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>)
}
</div>
</div>
</div>
</div>
);
}
Version.propTypes = {
dnsVersion: PropTypes.string,
dnsAddress: PropTypes.string,
dnsPort: PropTypes.number,
dnsVersion: PropTypes.string.isRequired,
dnsAddresses: PropTypes.array.isRequired,
dnsPort: PropTypes.number.isRequired,
};
export default withNamespaces()(Version);

View File

@@ -56,11 +56,13 @@ class Header extends Component {
toggleMenuOpen={this.toggleMenuOpen}
closeMenu={this.closeMenu}
/>
<div className="col col-sm-6 col-lg-3">
<Version
{ ...this.props.dashboard }
/>
</div>
{!dashboard.processing &&
<div className="col col-sm-6 col-lg-3">
<Version
{ ...this.props.dashboard }
/>
</div>
}
</div>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next';
import { formatTime } from '../../helpers/helpers';
import { formatTime, getClientName } from '../../helpers/helpers';
import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
@@ -86,7 +86,7 @@ class Logs extends Component {
}
renderLogs(logs) {
const { t } = this.props;
const { t, dashboard } = this.props;
const columns = [{
Header: t('time_table_header'),
accessor: 'time',
@@ -196,11 +196,19 @@ class Logs extends Component {
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const clientName = getClientName(dashboard.clients, row.value);
let client;
if (clientName) {
client = <span>{clientName} <small>({row.value})</small></span>;
} else {
client = row.value;
}
return (
<Fragment>
<div className="logs__row">
{row.value}
{client}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
@@ -315,9 +323,18 @@ class Logs extends Component {
</div>
</PageTitle>
<Card>
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
{queryLogEnabled && !queryLogs.getLogsProcessing &&
this.renderLogs(queryLogs.logs)}
{
queryLogEnabled
&& queryLogs.getLogsProcessing
&& dashboard.processingClients
&& <Loading />
}
{
queryLogEnabled
&& !queryLogs.getLogsProcessing
&& !dashboard.processingClients
&& this.renderLogs(queryLogs.logs)
}
</Card>
</Fragment>
);

View File

@@ -78,7 +78,8 @@ let Interface = (props) => {
</div>
{interfaceValue &&
<div className="col-sm-12 col-md-6">
{renderInterfaceValues(interfaces[interfaceValue])}
{interfaces[interfaceValue] &&
renderInterfaceValues(interfaces[interfaceValue])}
</div>
}
</div>

View File

@@ -1,32 +1,49 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withNamespaces } from 'react-i18next';
const columns = [{
Header: 'MAC',
accessor: 'mac',
}, {
Header: 'IP',
accessor: 'ip',
}, {
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',
}, {
Header: <Trans>dhcp_table_expires</Trans>,
accessor: 'expires',
}];
class Leases extends Component {
cellWrap = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
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"
/>
);
render() {
const { leases, t } = this.props;
return (
<ReactTable
data={leases || []}
columns={[
{
Header: 'MAC',
accessor: 'mac',
Cell: this.cellWrap,
}, {
Header: 'IP',
accessor: 'ip',
Cell: this.cellWrap,
}, {
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',
Cell: this.cellWrap,
}, {
Header: <Trans>dhcp_table_expires</Trans>,
accessor: 'expires',
Cell: this.cellWrap,
},
]}
showPagination={false}
noDataText={t('dhcp_leases_not_found')}
minRows={6}
className="-striped -highlight card-table-overflow"
/>
);
}
}
Leases.propTypes = {
leases: PropTypes.array,

View File

@@ -3,10 +3,12 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
import Form from './Form';
import Leases from './Leases';
import Interface from './Interface';
import Card from '../../ui/Card';
import Accordion from '../../ui/Accordion';
class Dhcp extends Component {
handleFormSubmit = (values) => {
@@ -19,11 +21,12 @@ class Dhcp extends Component {
getToggleDhcpButton = () => {
const {
config, active, processingDhcp, processingConfig,
config, check, processingDhcp, processingConfig,
} = this.props.dhcp;
const activeDhcpFound = active && active.found;
const otherDhcpFound =
check && check.otherServer && check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled') {
if (key === 'enabled' || key === 'icmp_timeout_msec') {
return true;
}
@@ -50,7 +53,8 @@ class Dhcp extends Component {
onClick={() => this.handleToggle(config)}
disabled={
!filledConfig
|| activeDhcpFound
|| !check
|| otherDhcpFound
|| processingDhcp
|| processingConfig
}
@@ -60,33 +64,89 @@ class Dhcp extends Component {
);
}
getActiveDhcpMessage = () => {
const { active } = this.props.dhcp;
if (active) {
if (active.error) {
return (
<div className="text-danger mb-2">
{active.error}
</div>
);
}
getActiveDhcpMessage = (t, check) => {
const { found } = check.otherServer;
if (found === DHCP_STATUS_RESPONSE.ERROR) {
return (
<div className="mb-2">
{active.found ? (
<div className="text-danger">
<Trans>dhcp_found</Trans>
</div>
) : (
<div className="text-secondary">
<Trans>dhcp_not_found</Trans>
</div>
)}
<div className="text-danger mb-2">
<Trans>dhcp_error</Trans>
<div className="mt-2 mb-2">
<Accordion label={t('error_details')}>
<span>{check.otherServer.error}</span>
</Accordion>
</div>
</div>
);
}
return (
<div className="mb-2">
{found === DHCP_STATUS_RESPONSE.YES ? (
<div className="text-danger">
<Trans>dhcp_found</Trans>
</div>
) : (
<div className="text-secondary">
<Trans>dhcp_not_found</Trans>
</div>
)}
</div>
);
}
getDhcpWarning = (check) => {
if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
return '';
}
return (
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
);
}
getStaticIpWarning = (t, check, interfaceName) => {
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
return (
<Fragment>
<div className="text-danger mb-2">
<Trans>dhcp_static_ip_error</Trans>
<div className="mt-2 mb-2">
<Accordion label={t('error_details')}>
<span>{check.staticIP.error}</span>
</Accordion>
</div>
</div>
<hr className="mt-4 mb-4"/>
</Fragment>
);
} else if (
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
&& check.staticIP.ip
&& interfaceName
) {
return (
<Fragment>
<div className="text-secondary mb-2">
<Trans
components={[
<strong key="0">example</strong>,
]}
values={{
interfaceName,
ipAddress: check.staticIP.ip,
}}
>
dhcp_dynamic_ip_found
</Trans>
</div>
<hr className="mt-4 mb-4"/>
</Fragment>
);
}
return '';
}
@@ -131,17 +191,21 @@ class Dhcp extends Component {
this.props.findActiveDhcp(dhcp.config.interface_name)
}
disabled={
!dhcp.config.interface_name
dhcp.config.enabled
|| !dhcp.config.interface_name
|| dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{this.getActiveDhcpMessage()}
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
{!enabled && dhcp.check &&
<Fragment>
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
{this.getActiveDhcpMessage(t, dhcp.check)}
{this.getDhcpWarning(dhcp.check)}
</Fragment>
}
</Fragment>
}
</div>

View File

@@ -57,6 +57,7 @@ let Form = (props) => {
valid_chain,
valid_key,
valid_cert,
valid_pair,
dns_names,
key_type,
issuer,
@@ -65,6 +66,15 @@ let Form = (props) => {
setTlsConfig,
} = props;
const isSavingDisabled = invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
|| (privateKey && certificateChain && !valid_pair);
return (
<form onSubmit={handleSubmit}>
<div className="row">
@@ -251,7 +261,7 @@ let Form = (props) => {
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder="Copy/paste your PEM-encoded private key for your cerficate here."
placeholder={t('encryption_key_input')}
onChange={handleChange}
disabled={!isEnabled}
/>
@@ -291,15 +301,7 @@ let Form = (props) => {
<button
type="submit"
className="btn btn-success btn-standart"
disabled={
invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
}
disabled={isSavingDisabled}
>
<Trans>save_config</Trans>
</button>
@@ -334,6 +336,7 @@ Form.propTypes = {
valid_chain: PropTypes.bool,
valid_key: PropTypes.bool,
valid_cert: PropTypes.bool,
valid_pair: PropTypes.bool,
dns_names: PropTypes.string,
key_type: PropTypes.string,
issuer: PropTypes.string,

View File

@@ -9,7 +9,9 @@ import Card from '../../ui/Card';
class Encryption extends Component {
componentDidMount() {
this.props.validateTlsConfig(this.props.encryption);
if (this.props.encryption.enabled) {
this.props.validateTlsConfig(this.props.encryption);
}
}
handleFormSubmit = (values) => {

View File

@@ -1,97 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
class Upstream extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleUpstreamChange(value);
};
handleSubmit = (e) => {
e.preventDefault();
this.props.handleUpstreamSubmit();
};
handleTest = () => {
this.props.handleUpstreamTest();
}
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<form>
<textarea
className="form-control form-control--textarea"
value={this.props.upstreamDns}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className={testButtonClass}
type="button"
onClick={this.handleTest}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
className="btn btn-success btn-standard"
type="submit"
onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr/>
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_dot') }} />
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_doh') }} />
</li>
<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>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
handleUpstreamChange: PropTypes.func,
handleUpstreamSubmit: PropTypes.func,
handleUpstreamTest: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(Upstream);

View File

@@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
const Examples = props => (
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> &nbsp;
<span>
<Trans
components={[
<a href="https://en.wikipedia.org/wiki/DNS_over_TLS" target="_blank" rel="noopener noreferrer" key="0">
DNS-over-TLS
</a>,
]}
>
example_upstream_dot
</Trans>
</span>
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> &nbsp;
<span>
<Trans
components={[
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="0">
DNS-over-HTTPS
</a>,
]}
>
example_upstream_doh
</Trans>
</span>
</li>
<li>
<code>tcp://1.1.1.1</code> <Trans>example_upstream_tcp</Trans>
</li>
<li>
<code>sdns://...</code> &nbsp;
<span>
<Trans
components={[
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
DNS Stamps
</a>,
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer" key="1">
DNSCrypt
</a>,
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="2">
DNS-over-HTTPS
</a>,
]}
>
example_upstream_sdns
</Trans>
</span>
</li>
<li>
<code>[/example.local/]1.1.1.1</code> &nbsp;
<span>
<Trans
components={[
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains" target="_blank" rel="noopener noreferrer" key="0">
Link
</a>,
]}
>
example_upstream_reserved
</Trans>
</span>
</li>
</ol>
</div>
);
Examples.propTypes = {
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Examples);

View File

@@ -0,0 +1,144 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import classnames from 'classnames';
import { renderSelectField } from '../../../helpers/form';
import Examples from './Examples';
let Form = (props) => {
const {
t,
handleSubmit,
testUpstream,
upstreamDns,
bootstrapDns,
allServers,
submitting,
invalid,
processingSetUpstream,
processingTestUpstream,
} = props;
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': processingTestUpstream,
});
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="upstream_dns">
<Trans>upstream_dns</Trans>
</label>
<Field
id="upstream_dns"
name="upstream_dns"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('upstream_dns')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="all_servers"
type="checkbox"
component={renderSelectField}
placeholder={t('upstream_parallel')}
/>
</div>
</div>
<div className="col-12">
<Examples />
<hr/>
</div>
<div className="col-12">
<div className="form__group">
<label className="form__label" htmlFor="bootstrap_dns">
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component="textarea"
type="text"
className="form-control"
placeholder={t('bootstrap_dns')}
/>
</div>
</div>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="button"
className={testButtonClass}
onClick={() => testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
})}
disabled={!upstreamDns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| processingSetUpstream
|| processingTestUpstream
}
>
<Trans>apply_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
testUpstream: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
processingTestUpstream: PropTypes.bool,
processingSetUpstream: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('upstreamForm');
Form = connect((state) => {
const upstreamDns = selector(state, 'upstream_dns');
const bootstrapDns = selector(state, 'bootstrap_dns');
const allServers = selector(state, 'all_servers');
return {
upstreamDns,
bootstrapDns,
allServers,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({ form: 'upstreamForm' }),
])(Form);

View File

@@ -0,0 +1,64 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Form from './Form';
import Card from '../../ui/Card';
class Upstream extends Component {
handleSubmit = (values) => {
this.props.setUpstream(values);
};
handleTest = (values) => {
this.props.testUpstream(values);
}
render() {
const {
t,
upstreamDns: upstream_dns,
bootstrapDns: bootstrap_dns,
allServers: all_servers,
processingSetUpstream,
processingTestUpstream,
} = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Form
initialValues={{
upstream_dns,
bootstrap_dns,
all_servers,
}}
testUpstream={this.handleTest}
onSubmit={this.handleSubmit}
processingTestUpstream={processingTestUpstream}
processingSetUpstream={processingSetUpstream}
/>
</div>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
processingSetUpstream: PropTypes.bool.isRequired,
processingTestUpstream: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Upstream);

View File

@@ -41,22 +41,6 @@ class Settings extends Component {
this.props.getTlsStatus();
}
handleUpstreamChange = (value) => {
this.props.handleUpstreamChange({ upstreamDns: value });
};
handleUpstreamSubmit = () => {
this.props.setUpstream(this.props.dashboard.upstreamDns);
};
handleUpstreamTest = () => {
if (this.props.dashboard.upstreamDns.length > 0) {
this.props.testUpstream(this.props.dashboard.upstreamDns);
} else {
this.props.addErrorToast({ error: this.props.t('no_servers_specified') });
}
};
renderSettings = (settings) => {
if (Object.keys(settings).length > 0) {
return Object.keys(settings).map((key) => {
@@ -75,8 +59,7 @@ class Settings extends Component {
}
render() {
const { settings, t } = this.props;
const { upstreamDns } = this.props.dashboard;
const { settings, dashboard, t } = this.props;
return (
<Fragment>
<PageTitle title={ t('settings') } />
@@ -91,11 +74,13 @@ class Settings extends Component {
</div>
</Card>
<Upstream
upstreamDns={upstreamDns}
upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
allServers={dashboard.allServers}
setUpstream={this.props.setUpstream}
testUpstream={this.props.testUpstream}
processingTestUpstream={settings.processingTestUpstream}
handleUpstreamChange={this.handleUpstreamChange}
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
processingSetUpstream={settings.processingSetUpstream}
/>
<Encryption
encryption={this.props.encryption}
@@ -125,7 +110,6 @@ Settings.propTypes = {
toggleSetting: PropTypes.func,
handleUpstreamChange: PropTypes.func,
setUpstream: PropTypes.func,
upstream: PropTypes.string,
t: PropTypes.func,
};

View File

@@ -0,0 +1,15 @@
.guide {
max-width: 768px;
margin: 0 auto;
}
.guide__title {
margin-bottom: 10px;
font-size: 17px;
font-weight: 700;
}
.guide__desc {
margin-bottom: 20px;
font-size: 15px;
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import { getDnsAddress } from '../../helpers/helpers';
import Guide from '../ui/Guide';
import Card from '../ui/Card';
import PageTitle from '../ui/PageTitle';
import './Guide.css';
const SetupGuide = ({
t,
dashboard: {
dnsAddresses,
dnsPort,
},
}) => (
<div className="guide">
<PageTitle title={t('setup_guide')} />
<Card>
<div className="guide__title">
<Trans>install_devices_title</Trans>
</div>
<div className="guide__desc">
<Trans>install_devices_desc</Trans>
<div className="mt-1">
<Trans>install_devices_address</Trans>:
</div>
<div className="mt-2 font-weight-bold">
{dnsAddresses
.map(ip => <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>)
}
</div>
</div>
<Guide />
</Card>
</div>
);
SetupGuide.propTypes = {
dashboard: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(SetupGuide);

View File

@@ -0,0 +1,32 @@
.accordion {
color: #495057;
}
.accordion__label {
position: relative;
display: inline-block;
padding-left: 25px;
cursor: pointer;
user-select: none;
}
.accordion__label:after {
content: "";
position: absolute;
top: 7px;
left: 0;
width: 17px;
height: 10px;
background-image: url("./svg/chevron-down.svg");
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
}
.accordion__label--open:after {
transform: rotate(180deg);
}
.accordion__content {
padding-top: 5px;
}

View File

@@ -0,0 +1,43 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './Accordion.css';
class Accordion extends Component {
state = {
isOpen: false,
}
handleClick = () => {
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
};
render() {
const accordionClass = this.state.isOpen
? 'accordion__label accordion__label--open'
: 'accordion__label';
return (
<div className="accordion">
<div
className={accordionClass}
onClick={this.handleClick}
>
{this.props.label}
</div>
{this.state.isOpen && (
<div className="accordion__content">
{this.props.children}
</div>
)}
</div>
);
}
}
Accordion.propTypes = {
children: PropTypes.node.isRequired,
label: PropTypes.string.isRequired,
};
export default Accordion;

View File

@@ -91,6 +91,10 @@
line-height: 1.5;
}
.checkbox__label-text--long {
max-width: initial;
}
.checkbox__label-title {
display: block;
line-height: 1.5;

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import { REPOSITORY, LANGUAGES } from '../../helpers/constants';
import { REPOSITORY, LANGUAGES, PRIVACY_POLICY_LINK } from '../../helpers/constants';
import i18n from '../../i18n';
import './Footer.css';
@@ -30,6 +30,9 @@ class Footer extends Component {
<a href={REPOSITORY.URL} className="footer__link" target="_blank" rel="noopener noreferrer">
<Trans>homepage</Trans>
</a>
<a href={PRIVACY_POLICY_LINK} className="footer__link" target="_blank" rel="noopener noreferrer">
<Trans>privacy_policy</Trans>
</a>
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm footer__link footer__link--report" target="_blank" rel="noopener noreferrer">
<Trans>report_an_issue</Trans>
</a>

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import Tabs from '../ui/Tabs';
import Icons from '../ui/Icons';
const Guide = () => (
<div>
<Icons />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_devices_router</Trans>
</div>
<div className="tab__text">
<p><Trans>install_devices_router_desc</Trans></p>
<ol>
<li><Trans>install_devices_router_list_1</Trans></li>
<li><Trans>install_devices_router_list_2</Trans></li>
<li><Trans>install_devices_router_list_3</Trans></li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_windows_list_1</Trans></li>
<li><Trans>install_devices_windows_list_2</Trans></li>
<li><Trans>install_devices_windows_list_3</Trans></li>
<li><Trans>install_devices_windows_list_4</Trans></li>
<li><Trans>install_devices_windows_list_5</Trans></li>
<li><Trans>install_devices_windows_list_6</Trans></li>
</ol>
</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_macos_list_1</Trans></li>
<li><Trans>install_devices_macos_list_2</Trans></li>
<li><Trans>install_devices_macos_list_3</Trans></li>
<li><Trans>install_devices_macos_list_4</Trans></li>
</ol>
</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_android_list_1</Trans></li>
<li><Trans>install_devices_android_list_2</Trans></li>
<li><Trans>install_devices_android_list_3</Trans></li>
<li><Trans>install_devices_android_list_4</Trans></li>
<li><Trans>install_devices_android_list_5</Trans></li>
</ol>
</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_ios_list_1</Trans></li>
<li><Trans>install_devices_ios_list_2</Trans></li>
<li><Trans>install_devices_ios_list_3</Trans></li>
<li><Trans>install_devices_ios_list_4</Trans></li>
</ol>
</div>
</div>
</Tabs>
</div>
);
export default withNamespaces()(Guide);

View File

@@ -22,6 +22,16 @@
height: 24px;
}
.popover__trigger--address {
top: 0;
margin: 0;
line-height: 1.2;
}
.popover__trigger--address:after {
display: none;
}
.popover__body {
content: "";
display: flex;
@@ -57,6 +67,38 @@
border-top: 6px solid #585965;
}
.popover__body--address {
top: calc(100% + 10px);
right: 0;
left: initial;
bottom: initial;
z-index: 1;
min-width: 100px;
padding: 12px 18px;
font-weight: 700;
text-align: left;
white-space: nowrap;
transform: none;
cursor: default;
}
.popover__body--address:after {
top: -11px;
left: initial;
right: 40px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}
.popover__body--address:before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 10px;
}
.popover__trigger:hover + .popover__body,
.popover__body:hover {
visibility: visible;
@@ -73,6 +115,10 @@
stroke: #66b574;
}
.popover__list--bold {
font-weight: 700;
}
.popover__list-title {
margin-bottom: 3px;
}

View File

@@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import * as actionCreators from '../actions';
import SetupGuide from '../components/SetupGuide';
const mapStateToProps = (state) => {
const { dashboard } = state;
const props = { dashboard };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(SetupGuide);

View File

@@ -22,6 +22,8 @@ export const REPOSITORY = {
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
};
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
export const LANGUAGES = [
{
key: 'en',
@@ -47,6 +49,10 @@ export const LANGUAGES = [
key: 'vi',
name: 'Tiếng Việt',
},
{
key: 'bg',
name: 'Български',
},
{
key: 'ru',
name: 'Русский',
@@ -59,6 +65,10 @@ export const LANGUAGES = [
key: 'zh-tw',
name: '正體中文',
},
{
key: 'zh-cn',
name: '简体中文',
},
];
export const INSTALL_FIRST_STEP = 1;
@@ -147,3 +157,11 @@ export const UNSAFE_PORTS = [
6668,
6669,
];
export const ALL_INTERFACES_IP = '0.0.0.0';
export const DHCP_STATUS_RESPONSE = {
YES: 'yes',
NO: 'no',
ERROR: 'error',
};

View File

@@ -32,7 +32,7 @@ export const renderSelectField = ({
disabled={disabled}
/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
</span>
</span>

View File

@@ -201,3 +201,10 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
}
};
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
export const getClientName = (clients, ip) => {
const client = clients.find(item => ip === item.ip);
return (client && client.name) || '';
};

View File

@@ -12,6 +12,8 @@ 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';
import bg from './__locales/bg.json';
import zhCN from './__locales/zh-cn.json';
const resources = {
en: {
@@ -41,6 +43,12 @@ const resources = {
'zh-TW': {
translation: zhTW,
},
bg: {
translation: bg,
},
'zh-CN': {
translation: zhCN,
},
};
i18n

View File

@@ -2,12 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
import { ALL_INTERFACES_IP } from '../../helpers/constants';
const AddressList = (props) => {
let webAddress = getWebAddress(props.address, props.port);
let dnsAddress = getDnsAddress(props.address, props.port);
if (props.address === '0.0.0.0') {
if (props.address === ALL_INTERFACES_IP) {
return getIpList(props.interfaces).map((ip) => {
webAddress = getWebAddress(ip, props.port);
dnsAddress = getDnsAddress(ip, props.port);

View File

@@ -25,13 +25,22 @@ class Controls extends Component {
}
renderNextButton(step) {
const {
nextStep,
invalid,
pristine,
install,
ip,
port,
} = this.props;
switch (step) {
case 1:
return (
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={this.props.nextStep}
onClick={nextStep}
>
<Trans>get_started</Trans>
</button>
@@ -43,9 +52,11 @@ class Controls extends Component {
type="submit"
className="btn btn-success btn-lg setup__button"
disabled={
this.props.invalid
|| this.props.pristine
|| this.props.install.processingSubmit
invalid
|| pristine
|| install.processingSubmit
|| install.dns.status
|| install.web.status
}
>
<Trans>next</Trans>
@@ -56,7 +67,7 @@ class Controls extends Component {
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={this.props.nextStep}
onClick={nextStep}
>
<Trans>next</Trans>
</button>
@@ -66,7 +77,8 @@ class Controls extends Component {
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={() => this.props.openDashboard(this.props.address)}
onClick={() =>
this.props.openDashboard(ip, port)}
>
<Trans>open_dashboard</Trans>
</button>
@@ -98,7 +110,8 @@ Controls.propTypes = {
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
address: PropTypes.string,
ip: PropTypes.string,
port: PropTypes.number,
};
const mapStateToProps = (state) => {

View File

@@ -5,8 +5,7 @@ import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import Tabs from '../../components/ui/Tabs';
import Icons from '../../components/ui/Icons';
import Guide from '../../components/ui/Guide';
import Controls from './Controls';
import AddressList from './AddressList';
@@ -30,77 +29,7 @@ let Devices = props => (
/>
</div>
</div>
<Icons />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_devices_router</Trans>
</div>
<div className="tab__text">
<p><Trans>install_devices_router_desc</Trans></p>
<ol>
<li><Trans>install_devices_router_list_1</Trans></li>
<li><Trans>install_devices_router_list_2</Trans></li>
<li><Trans>install_devices_router_list_3</Trans></li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_windows_list_1</Trans></li>
<li><Trans>install_devices_windows_list_2</Trans></li>
<li><Trans>install_devices_windows_list_3</Trans></li>
<li><Trans>install_devices_windows_list_4</Trans></li>
<li><Trans>install_devices_windows_list_5</Trans></li>
<li><Trans>install_devices_windows_list_6</Trans></li>
</ol>
</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_macos_list_1</Trans></li>
<li><Trans>install_devices_macos_list_2</Trans></li>
<li><Trans>install_devices_macos_list_3</Trans></li>
<li><Trans>install_devices_macos_list_4</Trans></li>
</ol>
</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_android_list_1</Trans></li>
<li><Trans>install_devices_android_list_2</Trans></li>
<li><Trans>install_devices_android_list_3</Trans></li>
<li><Trans>install_devices_android_list_4</Trans></li>
<li><Trans>install_devices_android_list_5</Trans></li>
</ol>
</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_ios_list_1</Trans></li>
<li><Trans>install_devices_ios_list_2</Trans></li>
<li><Trans>install_devices_ios_list_3</Trans></li>
<li><Trans>install_devices_ios_list_4</Trans></li>
</ol>
</div>
</div>
</Tabs>
<Guide />
</div>
<Controls />
</div>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
@@ -9,6 +9,7 @@ import Controls from './Controls';
import AddressList from './AddressList';
import renderField from './renderField';
import { getInterfaceIp } from '../../helpers/helpers';
import { ALL_INTERFACES_IP } from '../../helpers/constants';
const required = (value) => {
if (value || value === 0) {
@@ -29,10 +30,25 @@ const toNumber = value => value && parseInt(value, 10);
const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const {
name,
ip_addresses,
flags,
} = option;
if (option.ip_addresses && option.ip_addresses.length > 0) {
if (option && ip_addresses && ip_addresses.length > 0) {
const ip = getInterfaceIp(option);
const isDown = flags && flags.includes('down');
if (isDown) {
return (
<option value={ip} key={name} disabled>
<Fragment>
{name} - {ip} (<Trans>down</Trans>)
</Fragment>
</option>
);
}
return (
<option value={ip} key={name}>
@@ -45,141 +61,191 @@ const renderInterfaces = (interfaces => (
})
));
let Settings = (props) => {
const {
handleSubmit,
webIp,
webPort,
dnsIp,
dnsPort,
interfaces,
invalid,
webWarning,
dnsWarning,
} = props;
class Settings extends Component {
componentDidMount() {
const { web, dns } = this.props.config;
return (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_title</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="web.ip"
component="select"
className="form-control custom-select"
>
<option value="0.0.0.0">
<Trans>install_settings_all_interfaces</Trans>
</option>
{renderInterfaces(interfaces)}
</Field>
this.props.validateForm({
web,
dns,
});
}
render() {
const {
handleSubmit,
handleChange,
handleAutofix,
webIp,
webPort,
dnsIp,
dnsPort,
interfaces,
invalid,
config,
} = this.props;
const {
status: webStatus,
can_autofix: isWebFixAvailable,
} = config.web;
const {
status: dnsStatus,
can_autofix: isDnsFixAvailable,
} = config.dns;
return (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_title</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="web.ip"
component="select"
className="form-control custom-select"
onChange={handleChange}
>
<option value={ALL_INTERFACES_IP}>
<Trans>install_settings_all_interfaces</Trans>
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="web.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
onChange={handleChange}
/>
</div>
</div>
<div className="col-12">
{webStatus &&
<div className="setup__error text-danger">
{webStatus}
{isWebFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('web', webIp, webPort)}
>
<Trans>fix</Trans>
</button>
}
</div>
}
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="web.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={webIp}
port={webPort}
/>
</div>
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={webIp}
port={webPort}
/>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_dns</Trans>
</div>
{webWarning &&
<div className="text-danger mt-2">
{webWarning}
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="dns.ip"
component="select"
className="form-control custom-select"
onChange={handleChange}
>
<option value={ALL_INTERFACES_IP}>
<Trans>install_settings_all_interfaces</Trans>
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
</div>
}
</div>
</div>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_dns</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="dns.ip"
component="select"
className="form-control custom-select"
>
<option value="0.0.0.0">
<Trans>install_settings_all_interfaces</Trans>
</option>
{renderInterfaces(interfaces)}
</Field>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="dns.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
onChange={handleChange}
/>
</div>
</div>
<div className="col-12">
{dnsStatus &&
<div className="setup__error text-danger">
{dnsStatus}
{isDnsFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('dns', dnsIp, dnsPort)}
>
<Trans>fix</Trans>
</button>
}
</div>
}
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="dns.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
<div className="setup__desc">
<Trans>install_settings_dns_desc</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={dnsIp}
port={dnsPort}
isDns={true}
/>
</div>
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_dns_desc</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={dnsIp}
port={dnsPort}
isDns={true}
/>
</div>
{dnsWarning &&
<div className="text-danger mt-2">
{dnsWarning}
</div>
}
</div>
</div>
<Controls invalid={invalid} />
</form>
);
};
<Controls invalid={invalid} />
</form>
);
}
}
Settings.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
handleAutofix: PropTypes.func,
validateForm: PropTypes.func,
webIp: PropTypes.string.isRequired,
dnsIp: PropTypes.string.isRequired,
config: PropTypes.object.isRequired,
webPort: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
@@ -188,8 +254,6 @@ Settings.propTypes = {
PropTypes.string,
PropTypes.number,
]),
webWarning: PropTypes.string.isRequired,
dnsWarning: PropTypes.string.isRequired,
interfaces: PropTypes.object.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object,
@@ -197,7 +261,7 @@ Settings.propTypes = {
const selector = formValueSelector('install');
Settings = connect((state) => {
const SettingsForm = connect((state) => {
const webIp = selector(state, 'web.ip');
const webPort = selector(state, 'web.port');
const dnsIp = selector(state, 'dns.ip');
@@ -218,4 +282,4 @@ export default flow([
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Settings);
])(SettingsForm);

View File

@@ -115,3 +115,7 @@
padding-left: 30px;
padding-right: 30px;
}
.setup__error {
margin: -5px 0 5px;
}

View File

@@ -6,7 +6,6 @@ import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import Controls from './Controls';
import { getWebAddress } from '../../helpers/helpers';
let Submit = props => (
<div className="setup__step">
@@ -20,7 +19,8 @@ let Submit = props => (
</div>
<Controls
openDashboard={props.openDashboard}
address={getWebAddress(props.webIp, props.webPort)}
ip={props.webIp}
port={props.webPort}
/>
</div>
);

View File

@@ -1,9 +1,16 @@
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import * as actionCreators from '../../actions/install';
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
import { getWebAddress } from '../../helpers/helpers';
import {
INSTALL_FIRST_STEP,
INSTALL_TOTAL_STEPS,
ALL_INTERFACES_IP,
DEBOUNCE_TIMEOUT,
} from '../../helpers/constants';
import Loading from '../../components/ui/Loading';
import Greeting from './Greeting';
@@ -29,7 +36,37 @@ class Setup extends Component {
this.props.setAllSettings(values);
};
openDashboard = (address) => {
handleFormChange = debounce((values) => {
if (values && values.web.port && values.dns.port) {
this.props.checkConfig(values);
}
}, DEBOUNCE_TIMEOUT);
handleAutofix = (type, ip, port) => {
const data = {
ip,
port,
autofix: true,
};
if (type === 'web') {
this.props.checkConfig({
web: { ...data },
});
} else {
this.props.checkConfig({
dns: { ...data },
});
}
};
openDashboard = (ip, port) => {
let address = getWebAddress(ip, port);
if (ip === ALL_INTERFACES_IP) {
address = getWebAddress(window.location.hostname, port);
}
window.location.replace(address);
}
@@ -52,11 +89,13 @@ class Setup extends Component {
case 2:
return (
<Settings
config={config}
initialValues={config}
interfaces={interfaces}
webWarning={config.web.warning}
dnsWarning={config.dns.warning}
onSubmit={this.nextStep}
onChange={this.handleFormChange}
validateForm={this.handleFormChange}
handleAutofix={this.handleAutofix}
/>
);
case 3:
@@ -105,6 +144,7 @@ class Setup extends Component {
Setup.propTypes = {
getDefaultAddresses: PropTypes.func.isRequired,
setAllSettings: PropTypes.func.isRequired,
checkConfig: PropTypes.func.isRequired,
nextStep: PropTypes.func.isRequired,
prevStep: PropTypes.func.isRequired,
install: PropTypes.object.isRequired,

View File

@@ -70,6 +70,7 @@ const encryption = handleActions({
valid_chain: false,
valid_key: false,
valid_cert: false,
valid_pair: false,
status_cert: '',
status_key: '',
certificate_chain: '',

View File

@@ -48,9 +48,11 @@ const dashboard = handleActions({
version,
running,
dns_port: dnsPort,
dns_address: dnsAddress,
dns_addresses: dnsAddresses,
querylog_enabled: queryLogEnabled,
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
protection_enabled: protectionEnabled,
language,
http_port: httpPort,
@@ -61,9 +63,11 @@ const dashboard = handleActions({
processing: false,
dnsVersion: version,
dnsPort,
dnsAddress,
dnsAddresses,
queryLogEnabled,
upstreamDns: upstreamDns.join('\n'),
bootstrapDns: bootstrapDns.join('\n'),
allServers,
protectionEnabled,
language,
httpPort,
@@ -163,6 +167,17 @@ const dashboard = handleActions({
const newState = { ...state, language: payload };
return newState;
},
[actions.getClientsRequest]: state => ({ ...state, processingClients: true }),
[actions.getClientsFailure]: state => ({ ...state, processingClients: false }),
[actions.getClientsSuccess]: (state, { payload }) => {
const newState = {
...state,
clients: payload,
processingClients: false,
};
return newState;
},
}, {
processing: true,
isCoreRunning: false,
@@ -171,10 +186,17 @@ const dashboard = handleActions({
logStatusProcessing: false,
processingVersion: true,
processingFiltering: true,
upstreamDns: [],
processingClients: true,
upstreamDns: '',
bootstrapDns: '',
allServers: false,
protectionEnabled: false,
processingProtection: false,
httpPort: 80,
dnsPort: 53,
dnsAddresses: [],
dnsVersion: '',
clients: [],
});
const queryLogs = handleActions({
@@ -270,18 +292,31 @@ const dhcp = handleActions({
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
[actions.findActiveDhcpSuccess]: (state, { payload }) => ({
...state,
active: payload,
processingStatus: false,
}),
[actions.findActiveDhcpSuccess]: (state, { payload }) => {
const {
other_server: otherServer,
static_ip: staticIP,
} = payload;
const newState = {
...state,
check: {
otherServer,
staticIP,
},
processingStatus: false,
};
return newState;
},
[actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }),
[actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }),
[actions.toggleDhcpSuccess]: (state) => {
const { config } = state;
const newConfig = { ...config, enabled: !config.enabled };
const newState = { ...state, config: newConfig, processingDhcp: false };
const newState = {
...state, config: newConfig, check: null, processingDhcp: false,
};
return newState;
},
@@ -302,7 +337,7 @@ const dhcp = handleActions({
config: {
enabled: false,
},
active: null,
check: null,
leases: [],
});

View File

@@ -10,10 +10,13 @@ const install = handleActions({
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
const values = payload;
values.web.ip = state.web.ip;
values.dns.ip = state.dns.ip;
const newState = { ...state, ...values, processingDefault: false };
const { interfaces } = payload;
const web = { ...state.web, port: payload.web_port };
const dns = { ...state.dns, port: payload.dns_port };
const newState = {
...state, web, dns, interfaces, processingDefault: false,
};
return newState;
},
@@ -23,19 +26,34 @@ const install = handleActions({
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
[actions.checkConfigRequest]: state => ({ ...state, processingCheck: true }),
[actions.checkConfigFailure]: state => ({ ...state, processingCheck: false }),
[actions.checkConfigSuccess]: (state, { payload }) => {
const web = { ...state.web, ...payload.web };
const dns = { ...state.dns, ...payload.dns };
const newState = {
...state, web, dns, processingCheck: false,
};
return newState;
},
}, {
step: INSTALL_FIRST_STEP,
processingDefault: true,
processingSubmit: false,
processingCheck: false,
web: {
ip: '0.0.0.0',
port: 80,
warning: '',
status: '',
can_autofix: false,
},
dns: {
ip: '0.0.0.0',
port: 53,
warning: '',
status: '',
can_autofix: false,
},
interfaces: {},
});

View File

@@ -5,12 +5,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const flexBugsFixes = require('postcss-flexbugs-fixes');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
const FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.ico');
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
@@ -117,6 +119,9 @@ const config = {
new ExtractTextPlugin({
filename: '[name].[contenthash].css',
}),
new CopyPlugin([
{ from: FAVICON_PATH, to: PUBLIC_PATH },
]),
],
};

92
clients.go Normal file
View File

@@ -0,0 +1,92 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"runtime"
"strings"
"github.com/AdguardTeam/golibs/log"
)
// Client information
type Client struct {
IP string
Name string
//Source source // Hosts file / User settings / DHCP
}
type clientJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
}
var clients []Client
var clientsFilled bool
// Parse system 'hosts' file and fill clients array
func fillClientInfo() {
hostsFn := "/etc/hosts"
if runtime.GOOS == "windows" {
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
}
d, e := ioutil.ReadFile(hostsFn)
if e != nil {
log.Info("Can't read file %s: %v", hostsFn, e)
return
}
lines := strings.Split(string(d), "\n")
for _, ln := range lines {
ln = strings.TrimSpace(ln)
if len(ln) == 0 || ln[0] == '#' {
continue
}
fields := strings.Fields(ln)
if len(fields) < 2 {
continue
}
var c Client
c.IP = fields[0]
c.Name = fields[1]
clients = append(clients, c)
log.Tracef("%s -> %s", c.IP, c.Name)
}
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
clientsFilled = true
}
// respond with information about configured clients
func handleGetClients(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
if !clientsFilled {
fillClientInfo()
}
data := []clientJSON{}
for _, c := range clients {
cj := clientJSON{
IP: c.IP,
Name: c.Name,
}
data = append(data, cj)
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
return
}
}
// RegisterClientsHandlers registers HTTP handlers
func RegisterClientsHandlers() {
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
}

View File

@@ -4,13 +4,15 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
yaml "gopkg.in/yaml.v2"
)
@@ -32,11 +34,13 @@ type configuration struct {
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
Language string `yaml:"language"` // two-letter ISO 639-1 language code
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
Language string `yaml:"language"` // two-letter ISO 639-1 language code
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"`
@@ -60,7 +64,8 @@ type dnsConfig struct {
UpstreamDNS []string `yaml:"upstream_dns"`
}
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
var defaultDNS = []string{"https://dns.cloudflare.com/dns-query"}
var defaultBootstrap = []string{"1.1.1.1"}
type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
@@ -87,7 +92,7 @@ type tlsConfigStatus struct {
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator
usable bool
ValidPair bool `yaml:"-" json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
// warnings
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
@@ -114,7 +119,8 @@ var config = configuration{
QueryLogEnabled: true,
Ratelimit: 20,
RefuseAny: true,
BootstrapDNS: "8.8.8.8:53",
BootstrapDNS: defaultBootstrap,
AllServers: false,
},
UpstreamDNS: defaultDNS,
},
@@ -128,16 +134,36 @@ var config = configuration{
{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"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400,
ICMPTimeout: 1000,
},
SchemaVersion: currentSchemaVersion,
}
// init initializes default configuration for the current OS&ARCH
func init() {
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
// Use plain DNS on MIPS, encryption is too slow
defaultDNS = []string{"1.1.1.1", "1.0.0.1"}
// also change the default config
config.DNS.UpstreamDNS = defaultDNS
}
}
// getConfigFilename returns path to the current config file
func (c *configuration) getConfigFilename() string {
configFile := config.ourConfigFilename
configFile, err := filepath.EvalSymlinks(config.ourConfigFilename)
if err != nil {
if !os.IsNotExist(err) {
log.Error("unexpected error while config file path evaluation: %s", err)
}
configFile = config.ourConfigFilename
}
if !filepath.IsAbs(configFile) {
configFile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
configFile = filepath.Join(config.ourWorkingDir, configFile)
}
return configFile
}
@@ -152,7 +178,7 @@ func getLogSettings() logSettings {
}
err = yaml.Unmarshal(yamlFile, &l)
if err != nil {
log.Printf("Couldn't get logging settings from the configuration: %s", err)
log.Error("Couldn't get logging settings from the configuration: %s", err)
}
return l
}
@@ -160,19 +186,19 @@ func getLogSettings() logSettings {
// parseConfig loads configuration from the YAML file
func parseConfig() error {
configFile := config.getConfigFilename()
log.Printf("Reading config file: %s", configFile)
log.Debug("Reading config file: %s", configFile)
yamlFile, err := readConfigFile()
if err != nil {
log.Printf("Couldn't read config file: %s", err)
log.Error("Couldn't read config file: %s", err)
return err
}
if yamlFile == nil {
log.Printf("YAML file doesn't exist, skipping it")
log.Error("YAML file doesn't exist, skipping it")
return nil
}
err = yaml.Unmarshal(yamlFile, &config)
if err != nil {
log.Printf("Couldn't parse config file: %s", err)
log.Error("Couldn't parse config file: %s", err)
return err
}
@@ -199,19 +225,19 @@ func (c *configuration) write() error {
c.Lock()
defer c.Unlock()
if config.firstRun {
log.Tracef("Silently refusing to write config because first run and not configured yet")
log.Debug("Silently refusing to write config because first run and not configured yet")
return nil
}
configFile := config.getConfigFilename()
log.Tracef("Writing YAML file: %s", configFile)
log.Debug("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
if err != nil {
log.Printf("Couldn't generate YAML file: %s", err)
log.Error("Couldn't generate YAML file: %s", err)
return err
}
err = safeWriteFile(configFile, yamlText)
err = file.SafeWrite(configFile, yamlText)
if err != nil {
log.Printf("Couldn't save YAML config: %s", err)
log.Error("Couldn't save YAML config: %s", err)
return err
}
@@ -221,14 +247,14 @@ func (c *configuration) write() error {
func writeAllConfigs() error {
err := config.write()
if err != nil {
log.Printf("Couldn't write config: %s", err)
log.Error("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)
log.Error("Couldn't save the user filter: %s", err)
return err
}

File diff suppressed because it is too large Load Diff

278
control_install.go Normal file
View File

@@ -0,0 +1,278 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os/exec"
"strconv"
"github.com/AdguardTeam/golibs/log"
)
type firstRunData struct {
WebPort int `json:"web_port"`
DNSPort int `json:"dns_port"`
Interfaces map[string]interface{} `json:"interfaces"`
}
// Get initial installation settings
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data := firstRunData{}
data.WebPort = 80
data.DNSPort = 53
ifaces, err := getValidNetInterfacesForWeb()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
}
data.Interfaces = make(map[string]interface{})
for _, iface := range ifaces {
data.Interfaces[iface.Name] = iface
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
return
}
}
type checkConfigReqEnt struct {
Port int `json:"port"`
IP string `json:"ip"`
Autofix bool `json:"autofix"`
}
type checkConfigReq struct {
Web checkConfigReqEnt `json:"web"`
DNS checkConfigReqEnt `json:"dns"`
}
type checkConfigRespEnt struct {
Status string `json:"status"`
CanAutofix bool `json:"can_autofix"`
}
type checkConfigResp struct {
Web checkConfigRespEnt `json:"web"`
DNS checkConfigRespEnt `json:"dns"`
}
// Check if ports are available, respond with results
func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
reqData := checkConfigReq{}
respData := checkConfigResp{}
err := json.NewDecoder(r.Body).Decode(&reqData)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err)
return
}
if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port)
if err != nil {
respData.Web.Status = fmt.Sprintf("%v", err)
}
}
if reqData.DNS.Port != 0 {
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
if errorIsAddrInUse(err) {
canAutofix := checkDNSStubListener()
if canAutofix && reqData.DNS.Autofix {
err = disableDNSStubListener()
if err != nil {
log.Error("Couldn't disable DNSStubListener: %s", err)
}
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
canAutofix = false
}
respData.DNS.CanAutofix = canAutofix
}
if err == nil {
err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
}
if err != nil {
respData.DNS.Status = fmt.Sprintf("%v", err)
}
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(respData)
if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err)
return
}
}
// Check if DNSStubListener is active
func checkDNSStubListener() bool {
cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved")
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
log.Error("command %s has failed: %v code:%d",
cmd.Path, err, cmd.ProcessState.ExitCode())
return false
}
cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf")
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err = cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
log.Error("command %s has failed: %v code:%d",
cmd.Path, err, cmd.ProcessState.ExitCode())
return false
}
return true
}
// Deactivate DNSStubListener
func disableDNSStubListener() error {
cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf")
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err := cmd.Output()
if err != nil {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("process %s exited with an error: %d",
cmd.Path, cmd.ProcessState.ExitCode())
}
cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved")
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err = cmd.Output()
if err != nil {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("process %s exited with an error: %d",
cmd.Path, cmd.ProcessState.ExitCode())
}
return nil
}
type applyConfigReqEnt struct {
IP string `json:"ip"`
Port int `json:"port"`
}
type applyConfigReq struct {
Web applyConfigReqEnt `json:"web"`
DNS applyConfigReqEnt `json:"dns"`
Username string `json:"username"`
Password string `json:"password"`
}
// Copy installation parameters between two configuration objects
func copyInstallSettings(dst *configuration, src *configuration) {
dst.BindHost = src.BindHost
dst.BindPort = src.BindPort
dst.DNS.BindHost = src.DNS.BindHost
dst.DNS.Port = src.DNS.Port
dst.AuthName = src.AuthName
dst.AuthPass = src.AuthPass
}
// Apply new configuration, start DNS server, restart Web server
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
newSettings := applyConfigReq{}
err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
return
}
if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 {
httpError(w, http.StatusBadRequest, "port value can't be 0")
return
}
restartHTTP := true
if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
// no need to rebind
restartHTTP = false
}
// validate that hosts and ports are bindable
if restartHTTP {
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s",
net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
return
}
}
err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
var curConfig configuration
copyInstallSettings(&curConfig, &config)
config.firstRun = false
config.BindHost = newSettings.Web.IP
config.BindPort = newSettings.Web.Port
config.DNS.BindHost = newSettings.DNS.IP
config.DNS.Port = newSettings.DNS.Port
config.AuthName = newSettings.Username
config.AuthPass = newSettings.Password
err = startDNSServer()
if err != nil {
config.firstRun = true
copyInstallSettings(&config, &curConfig)
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
return
}
err = config.write()
if err != nil {
config.firstRun = true
copyInstallSettings(&config, &curConfig)
httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
return
}
go refreshFiltersIfNecessary(false)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTP {
go func() {
httpServer.Shutdown(context.TODO())
}()
}
returnOK(w)
}
func registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
}

153
control_test.go Normal file
View File

@@ -0,0 +1,153 @@
package main
import (
"testing"
"time"
)
/* Tests performed:
. Bad certificate
. Bad private key
. Valid certificate & private key */
func TestValidateCertificates(t *testing.T) {
var data tlsConfigStatus
// bad cert
data = validateCertificates("bad cert", "", "")
if !(data.WarningValidation != "" &&
!data.ValidCert &&
!data.ValidChain) {
t.Fatalf("bad cert: validateCertificates(): %v", data)
}
// bad priv key
data = validateCertificates("", "bad priv key", "")
if !(data.WarningValidation != "" &&
!data.ValidKey) {
t.Fatalf("bad priv key: validateCertificates(): %v", data)
}
// valid cert & priv key
CertificateChain := `-----BEGIN CERTIFICATE-----
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
MBMGA1UEAwwMQWRHdWFyZCBIb21lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQCwvwUnPJiOvLcOaWmGu6Y68ksFr13nrXBcsDlhxlXy8PaohVi3XxEmt2OrVjKW
QFw/bdV4fZ9tdWFAVRRkgeGbIZzP7YBD1Ore/O5SQ+DbCCEafvjJCcXQIrTeKFE6
i9G3aSMHs0Pwq2LgV8U5mYotLrvyFiE8QPInJbDDMpaFYwIDAQABo1MwUTAdBgNV
HQ4EFgQUdLUmQpEqrhn4eKO029jYd2AAZEQwHwYDVR0jBBgwFoAUdLUmQpEqrhn4
eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
-----END CERTIFICATE-----`
PrivateKey := `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
xTmZii0uu/IWITxA8iclsMMyloVjAgMBAAECgYEAmjzoG1h27UDkIlB9BVWl95TP
QVPLB81D267xNFDnWk1Lgr5zL/pnNjkdYjyjgpkBp1yKyE4gHV4skv5sAFWTcOCU
QCgfPfUn/rDFcxVzAdJVWAa/CpJNaZgjTPR8NTGU+Ztod+wfBESNCP5tbnuw0GbL
MuwdLQJGbzeJYpsNysECQQDfFHYoRNfgxHwMbX24GCoNZIgk12uDmGTA9CS5E+72
9t3V1y4CfXxSkfhqNbd5RWrUBRLEw9BKofBS7L9NMDKDAkEAytQoIueE1vqEAaRg
a3A1YDUekKesU5wKfKfKlXvNgB7Hwh4HuvoQS9RCvVhf/60Dvq8KSu6hSjkFRquj
FQ5roQJBAMwKwyiCD5MfJPeZDmzcbVpiocRQ5Z4wPbffl9dRTDnIA5AciZDthlFg
An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
kXS9jgARhhiWXJrk
-----END PRIVATE KEY-----`
data = validateCertificates(CertificateChain, PrivateKey, "")
notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z")
notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z")
if !(data.WarningValidation != "" /* self signed */ &&
data.ValidCert &&
!data.ValidChain &&
data.ValidKey &&
data.KeyType == "RSA" &&
data.Subject == "CN=AdGuard Home,O=AdGuard Ltd" &&
data.Issuer == "CN=AdGuard Home,O=AdGuard Ltd" &&
data.NotBefore == notBefore &&
data.NotAfter == notAfter &&
// data.DNSNames[0] == &&
data.ValidPair) {
t.Fatalf("valid cert & priv key: validateCertificates(): %v", data)
}
}
func TestValidateUpstream(t *testing.T) {
invalidUpstreams := []string{"1.2.3.4.5",
"123.3.7m",
"htttps://google.com/dns-query",
"[/host.com]tls://dns.adguard.com",
"[host.ru]#",
}
validDefaultUpstreams := []string{"1.1.1.1",
"tls://1.1.1.1",
"https://dns.adguard.com/dns-query",
"sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
validUpstreams := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
for _, u := range invalidUpstreams {
_, err := validateUpstream(u)
if err == nil {
t.Fatalf("upstream %s is invalid but it pass through validation", u)
}
}
for _, u := range validDefaultUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if !defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
for _, u := range validUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
}
func TestValidateUpstreamsSet(t *testing.T) {
// Set of valid upstreams. There is no default upstream specified
upstreamsSet := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
err := validateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is no default upstream")
}
// Let's add default upstream
upstreamsSet = append(upstreamsSet, "8.8.8.8")
err = validateUpstreams(upstreamsSet)
if err != nil {
t.Fatalf("upstreams set is valid, but doesn't pass through validation cause: %s", err)
}
// Let's add invalid upstream
upstreamsSet = append(upstreamsSet, "dhcp://fake.dns")
err = validateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is an invalid upstream in set, but it pass through validation")
}
}

338
control_tls.go Normal file
View File

@@ -0,0 +1,338 @@
// Control: TLS configuring handlers
package main
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
// RegisterTLSHandlers registers HTTP handlers for TLS configuration
func RegisterTLSHandlers() {
http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus))))
http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure))))
http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate))))
}
func handleTLSStatus(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
marshalTLS(w, config.TLS)
}
func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
marshalTLS(w, data)
}
func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
restartHTTPS := false
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
log.Printf("tls config settings have changed, will restart HTTPS server")
restartHTTPS = true
}
config.TLS = data
err = writeAllConfigsAndReloadDNS()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
return
}
marshalTLS(w, data)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTPS {
go func() {
time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
httpsServer.cond.L.Lock()
httpsServer.cond.Broadcast()
if httpsServer.server != nil {
httpsServer.server.Shutdown(context.TODO())
}
httpsServer.cond.L.Unlock()
}()
}
}
func verifyCertChain(data *tlsConfigStatus, certChain string, serverName string) error {
log.Tracef("got certificate: %s", certChain)
// now do a more extended validation
var certs []*pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
pemblock := []byte(certChain)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "CERTIFICATE" {
certs = append(certs, decoded)
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
var parsedCerts []*x509.Certificate
for _, cert := range certs {
parsed, err := x509.ParseCertificate(cert.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
return errors.New(data.WarningValidation)
}
parsedCerts = append(parsedCerts, parsed)
}
if len(parsedCerts) == 0 {
data.WarningValidation = fmt.Sprintf("You have specified an empty certificate")
return errors.New(data.WarningValidation)
}
data.ValidCert = true
// spew.Dump(parsedCerts)
opts := x509.VerifyOptions{
DNSName: serverName,
}
log.Printf("number of certs - %d", len(parsedCerts))
if len(parsedCerts) > 1 {
// set up an intermediate
pool := x509.NewCertPool()
for _, cert := range parsedCerts[1:] {
log.Printf("got an intermediate cert")
pool.AddCert(cert)
}
opts.Intermediates = pool
}
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
mainCert := parsedCerts[0]
_, err := mainCert.Verify(opts)
if err != nil {
// let self-signed certs through
data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
} else {
data.ValidChain = true
}
// spew.Dump(chains)
// update status
if mainCert != nil {
notAfter := mainCert.NotAfter
data.Subject = mainCert.Subject.String()
data.Issuer = mainCert.Issuer.String()
data.NotAfter = notAfter
data.NotBefore = mainCert.NotBefore
data.DNSNames = mainCert.DNSNames
}
return nil
}
func validatePkey(data *tlsConfigStatus, pkey string) error {
// now do a more extended validation
var key *pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
// go through all pem blocks, but take first valid pem block and drop the rest
pemblock := []byte(pkey)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
key = decoded
break
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
if key == nil {
data.WarningValidation = "No valid keys were found"
return errors.New(data.WarningValidation)
}
// parse the decoded key
_, keytype, err := parsePrivateKey(key.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
return errors.New(data.WarningValidation)
}
data.ValidKey = true
data.KeyType = keytype
return nil
}
// Process certificate data and its private key.
// All parameters are optional.
// On error, return partially set object
// with 'WarningValidation' field containing error description.
func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
var data tlsConfigStatus
// check only public certificate separately from the key
if certChain != "" {
if verifyCertChain(&data, certChain, serverName) != nil {
return data
}
}
// validate private key (right now the only validation possible is just parsing it)
if pkey != "" {
if validatePkey(&data, pkey) != nil {
return data
}
}
// if both are set, validate both in unison
if pkey != "" && certChain != "" {
_, err := tls.X509KeyPair([]byte(certChain), []byte(pkey))
if err != nil {
data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
return data
}
data.ValidPair = true
}
return data
}
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, "RSA", nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, "RSA", nil
case *ecdsa.PrivateKey:
return key, "ECDSA", nil
default:
return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, "ECDSA", nil
}
return nil, "", errors.New("tls: failed to parse private key")
}
// unmarshalTLS handles base64-encoded certificates transparently
func unmarshalTLS(r *http.Request) (tlsConfig, error) {
data := tlsConfig{}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
return data, errorx.Decorate(err, "Failed to parse new TLS config json")
}
if data.CertificateChain != "" {
certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
}
data.CertificateChain = string(certPEM)
}
if data.PrivateKey != "" {
keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode private key")
}
data.PrivateKey = string(keyPEM)
}
return data, nil
}
func marshalTLS(w http.ResponseWriter, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded
}
if data.PrivateKey != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey))
data.PrivateKey = encoded
}
err := json.NewEncoder(w).Encode(data)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err)
return
}
}

205
dhcp.go
View File

@@ -2,21 +2,26 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
var dhcpServer = dhcpd.Server{}
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
rawLeases := dhcpServer.Leases()
leases := []map[string]string{}
for i := range rawLeases {
@@ -43,6 +48,7 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
}
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
newconfig := dhcpd.ServerConfig{}
err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil {
@@ -50,24 +56,35 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
err = dhcpServer.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
if newconfig.Enabled {
err := dhcpServer.Start(&newconfig)
staticIP, err := hasStaticIP(newconfig.InterfaceName)
if !staticIP && err == nil {
err = setStaticIP(newconfig.InterfaceName)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
return
}
}
err = dhcpServer.Start(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
}
}
if !newconfig.Enabled {
err := dhcpServer.Stop()
if err != nil {
log.Printf("failed to stop the DHCP server: %s", err)
}
}
config.DHCP = newconfig
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
response := map[string]interface{}{}
ifaces, err := getValidNetInterfaces()
@@ -127,11 +144,16 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
}
}
// Perform the following tasks:
// . Search for another DHCP server running
// . Check if a static IP is configured for the network interface
// Respond with results
func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
errorText := fmt.Sprintf("failed to read request body: %s", err)
log.Println(errorText)
log.Error(errorText)
http.Error(w, errorText, http.StatusBadRequest)
return
}
@@ -139,17 +161,39 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
interfaceName := strings.TrimSpace(string(body))
if interfaceName == "" {
errorText := fmt.Sprintf("empty interface name specified")
log.Println(errorText)
log.Error(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
othSrv := map[string]interface{}{}
foundVal := "no"
if found {
foundVal = "yes"
} else if err != nil {
foundVal = "error"
othSrv["error"] = err.Error()
}
othSrv["found"] = foundVal
staticIP := map[string]interface{}{}
isStaticIP, err := hasStaticIP(interfaceName)
staticIPStatus := "yes"
if err != nil {
staticIPStatus = "error"
staticIP["error"] = err.Error()
} else if !isStaticIP {
staticIPStatus = "no"
staticIP["ip"] = getFullIP(interfaceName)
}
staticIP["static"] = staticIPStatus
result := map[string]interface{}{}
result["other_server"] = othSrv
result["static_ip"] = staticIP
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
@@ -158,6 +202,137 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
}
}
// Check if network interface has a static IP configured
func hasStaticIP(ifaceName string) (bool, error) {
if runtime.GOOS == "windows" {
return false, errors.New("Can't detect static IP: not supported on Windows")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return false, err
}
lines := strings.Split(string(body), "\n")
nameLine := fmt.Sprintf("interface %s", ifaceName)
withinInterfaceCtx := false
for _, line := range lines {
line = strings.TrimSpace(line)
if withinInterfaceCtx && len(line) == 0 {
// an empty line resets our state
withinInterfaceCtx = false
}
if len(line) == 0 || line[0] == '#' {
continue
}
line = strings.TrimSpace(line)
if !withinInterfaceCtx {
if line == nameLine {
// we found our interface
withinInterfaceCtx = true
}
} else {
if strings.HasPrefix(line, "interface ") {
// we found another interface - reset our state
withinInterfaceCtx = false
continue
}
if strings.HasPrefix(line, "static ip_address=") {
return true, nil
}
}
}
return false, nil
}
// Get IP address with netmask
func getFullIP(ifaceName string) string {
cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName)
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
d, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return ""
}
fields := strings.Fields(string(d))
if len(fields) < 4 {
return ""
}
_, _, err = net.ParseCIDR(fields[3])
if err != nil {
return ""
}
return fields[3]
}
// Get gateway IP address
func getGatewayIP(ifaceName string) string {
cmd := exec.Command("ip", "route", "show", "dev", ifaceName)
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
d, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return ""
}
fields := strings.Fields(string(d))
if len(fields) < 3 || fields[0] != "default" {
return ""
}
ip := net.ParseIP(fields[2])
if ip == nil {
return ""
}
return fields[2]
}
// Set a static IP for network interface
func setStaticIP(ifaceName string) error {
ip := getFullIP(ifaceName)
if len(ip) == 0 {
return errors.New("Can't get IP address")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return err
}
ip4, _, err := net.ParseCIDR(ip)
if err != nil {
return err
}
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
ifaceName, ip)
body = append(body, []byte(add)...)
gatewayIP := getGatewayIP(ifaceName)
if len(gatewayIP) != 0 {
add = fmt.Sprintf("static routers=%s\n",
gatewayIP)
body = append(body, []byte(add)...)
}
add = fmt.Sprintf("static domain_name_servers=%s\n\n",
ip4)
body = append(body, []byte(add)...)
err = file.SafeWrite("/etc/dhcpcd.conf", body)
if err != nil {
return err
}
return nil
}
func startDHCPServer() error {
if !config.DHCP.Enabled {
// not enabled, don't do anything

53
dhcpd/README.md Normal file
View File

@@ -0,0 +1,53 @@
# DHCP server
Contents:
* [Test setup with Virtual Box](#vbox)
<a id="vbox"></a>
## Test setup with Virtual Box
To set up a test environment for DHCP server you need:
* Linux host machine
* Virtual Box
* Virtual machine (guest OS doesn't matter)
### Configure client
1. Install Virtual Box and run the following command to create a Host-Only network:
$ VBoxManage hostonlyif create
You can check its status by `ip a` command.
You can also set up Host-Only network using Virtual Box menu:
File -> Host Network Manager...
2. Create your virtual machine and set up its network:
VM Settings -> Network -> Host-only Adapter
3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server.
### Configure server
1. Edit server configuration file 'AdGuardHome.yaml', for example:
dhcp:
enabled: true
interface_name: vboxnet0
gateway_ip: 192.168.56.1
subnet_mask: 255.255.255.0
range_start: 192.168.56.2
range_end: 192.168.56.2
lease_duration: 86400
icmp_timeout_msec: 1000
2. Start the server
./AdGuardHome
There should be a message in log which shows that DHCP server is ready:
[info] DHCP: listening on 0.0.0.0:67

View File

@@ -1,6 +1,7 @@
package dhcpd
import (
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
@@ -9,8 +10,9 @@ import (
"os"
"time"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
"golang.org/x/net/ipv4"
)
// CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface,
@@ -32,13 +34,13 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
dst := "255.255.255.255:67"
// form a DHCP request packet, try to emulate existing client as much as possible
xID := make([]byte, 8)
xID := make([]byte, 4)
n, err := rand.Read(xID)
if n != 8 && err == nil {
err = fmt.Errorf("Generated less than 8 bytes")
if n != 4 && err == nil {
err = fmt.Errorf("Generated less than 4 bytes")
}
if err != nil {
return false, wrapErrPrint(err, "Couldn't generate 8 random bytes")
return false, wrapErrPrint(err, "Couldn't generate random bytes")
}
hostname, err := os.Hostname()
if err != nil {
@@ -89,58 +91,63 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
// bind to 0.0.0.0:68
log.Tracef("Listening to udp4 %+v", udpAddr)
c, err := net.ListenPacket("udp4", src)
c, err := newBroadcastPacketConn(net.IPv4(0, 0, 0, 0), 68, ifaceName)
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)
return false, wrapErrPrint(err, "Couldn't listen on :68")
}
// send to 255.255.255.255:67
_, err = c.WriteTo(packet, dstAddr)
cm := ipv4.ControlMessage{}
_, err = c.WriteTo(packet, &cm, 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)
for {
// 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")
}
// 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")
}
log.Tracef("Received packet (%v bytes)", n)
response := dhcp4.Packet(b[:n])
if response.HLen() > 16 {
// invalid size
return false, wrapErrPrint(err, "got malformed packet with HLen() > 16")
}
if n < 240 {
// packet too small for dhcp
continue
}
parsedOptions := response.ParseOptions()
_, ok := parsedOptions[dhcp4.OptionDHCPMessageType]
if !ok {
return false, wrapErrPrint(err, "got malformed packet without DHCP message type")
}
response := dhcp4.Packet(b[:n])
if response.OpCode() != dhcp4.BootReply ||
response.HType() != 1 /*Ethernet*/ ||
response.HLen() > 16 ||
!bytes.Equal(response.CHAddr(), iface.HardwareAddr) ||
!bytes.Equal(response.XId(), xID) {
continue
}
// that's a DHCP server there
return true, nil
parsedOptions := response.ParseOptions()
if t := parsedOptions[dhcp4.OptionDHCPMessageType]; len(t) != 1 {
continue //packet without DHCP message type
}
log.Tracef("The packet is from an active DHCP server")
// that's a DHCP server there
return true, nil
}
}

98
dhcpd/db.go Normal file
View File

@@ -0,0 +1,98 @@
// On-disk database for lease table
package dhcpd
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"time"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
)
const dbFilename = "leases.db"
type leaseJSON struct {
HWAddr []byte `json:"mac"`
IP []byte `json:"ip"`
Hostname string `json:"host"`
Expiry int64 `json:"exp"`
}
// Load lease table from DB
func (s *Server) dbLoad() {
data, err := ioutil.ReadFile(dbFilename)
if err != nil {
if !os.IsNotExist(err) {
log.Error("DHCP: can't read file %s: %v", dbFilename, err)
}
return
}
obj := []leaseJSON{}
err = json.Unmarshal(data, &obj)
if err != nil {
log.Error("DHCP: invalid DB: %v", err)
return
}
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
numLeases := len(obj)
for i := range obj {
if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
continue
}
lease := Lease{
HWAddr: obj[i].HWAddr,
IP: obj[i].IP,
Hostname: obj[i].Hostname,
Expiry: time.Unix(obj[i].Expiry, 0),
}
s.leases = append(s.leases, &lease)
s.reserveIP(lease.IP, lease.HWAddr)
}
log.Info("DHCP: loaded %d leases from DB", numLeases)
}
// Store lease table in DB
func (s *Server) dbStore() {
var leases []leaseJSON
for i := range s.leases {
if s.leases[i].Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: s.leases[i].HWAddr,
IP: s.leases[i].IP,
Hostname: s.leases[i].Hostname,
Expiry: s.leases[i].Expiry.Unix(),
}
leases = append(leases, lease)
}
data, err := json.Marshal(leases)
if err != nil {
log.Error("json.Marshal: %v", err)
return
}
err = file.SafeWrite(dbFilename, data)
if err != nil {
log.Error("DHCP: can't store lease table on disk: %v filename: %s",
err, dbFilename)
return
}
log.Info("DHCP: stored %d leases in DB", len(leases))
}

View File

@@ -4,11 +4,13 @@ import (
"bytes"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
ping "github.com/sparrc/go-ping"
)
const defaultDiscoverTime = time.Second * 3
@@ -32,6 +34,10 @@ type ServerConfig struct {
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
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
}
// Server - the current state of the DHCP server
@@ -40,6 +46,11 @@ type Server struct {
ipnet *net.IPNet // if interface name changes, this needs to be reset
cond *sync.Cond // Synchronize worker thread with main thread
mutex sync.Mutex // Mutex for 'cond'
running bool // Set if the worker thread is running
stopping bool // Set if the worker thread should be stopped
// leases
leases []*Lease
leaseStart net.IP // parsed from config RangeStart
@@ -54,6 +65,16 @@ type Server struct {
sync.RWMutex
}
// Print information about the available network interfaces
func printInterfaces() {
ifaces, _ := net.Interfaces()
var buf strings.Builder
for i := range ifaces {
buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name))
}
log.Info("Available network interfaces: %s", buf.String())
}
// 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 {
@@ -64,6 +85,7 @@ func (s *Server) Start(config *ServerConfig) error {
iface, err := net.InterfaceByName(s.InterfaceName)
if err != nil {
s.closeConn() // in case it was already started
printInterfaces()
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
}
@@ -122,20 +144,27 @@ func (s *Server) Start(config *ServerConfig) error {
s.closeConn()
}
s.dbLoad()
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")
}
log.Info("DHCP: listening on 0.0.0.0:67")
s.conn = c
s.cond = sync.NewCond(&s.mutex)
s.running = true
go func() {
// operate on c instead of c.conn because c.conn can change over time
err := dhcp4.Serve(c, s)
if err != nil {
if err != nil && !s.stopping {
log.Printf("dhcp4.Serve() returned with error: %s", err)
}
c.Close() // in case Serve() exits for other reason than listening socket closure
s.running = false
s.cond.Signal()
}()
return nil
@@ -147,11 +176,23 @@ func (s *Server) Stop() error {
// nothing to do, return silently
return nil
}
s.stopping = true
err := s.closeConn()
if err != nil {
return wrapErrPrint(err, "Couldn't close UDP listening socket")
}
// We've just closed the listening socket.
// Worker thread should exit right after it tries to read from the socket.
s.mutex.Lock()
for s.running {
s.cond.Wait()
}
s.mutex.Unlock()
s.dbStore()
return nil
}
@@ -165,6 +206,7 @@ func (s *Server) closeConn() error {
return err
}
// Reserve a lease for the client
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
@@ -172,27 +214,39 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
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
hostname := p.ParseOptions()[dhcp4.OptionHostName]
lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)}
log.Tracef("Lease not found for %s: creating new one", hwaddr)
ip, err := s.findFreeIP(hwaddr)
if err != nil {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
i := s.findExpiredLease()
if i < 0 {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
}
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
lease.IP = s.leases[i].IP
s.Lock()
s.leases[i] = lease
s.Unlock()
s.reserveIP(lease.IP, hwaddr)
return lease, nil
}
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)}
lease.IP = ip
s.Lock()
s.leases = append(s.leases, lease)
s.Unlock()
return lease, nil
}
func (s *Server) locateLease(p dhcp4.Packet) *Lease {
// Find a lease for the client
func (s *Server) findLease(p dhcp4.Packet) *Lease {
hwaddr := p.CHAddr()
for i := range s.leases {
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
@@ -203,6 +257,17 @@ func (s *Server) locateLease(p dhcp4.Packet) *Lease {
return nil
}
// Find an expired lease and return its index or -1
func (s *Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() <= now {
return i
}
}
return -1
}
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
// if IP pool is nil, lazy initialize it
if s.IPpool == nil {
@@ -213,13 +278,12 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
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)
foundHWaddr := s.findReservedHWaddr(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
@@ -236,7 +300,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
return foundIP, nil
}
func (s *Server) getIPpool(ip net.IP) net.HardwareAddr {
func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
return s.IPpool[IP4]
@@ -256,133 +320,230 @@ func (s *Server) unreserveIP(ip net.IP) {
// ServeDHCP handles an incoming DHCP request
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)
}
s.printLeases()
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
return s.handleDiscover(p, options)
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
return s.handleDHCP4Request(p, options)
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
log.Tracef("Got from client: Decline")
return s.handleDecline(p, options)
case dhcp4.Release: // From Client, I don't need that IP anymore
log.Tracef("Got from client: Release")
return s.handleRelease(p, options)
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
return s.handleInform(p, options)
// 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")
log.Printf("DHCP: received message from %s: Offer", p.CHAddr())
case dhcp4.ACK: // From Server, Yes you can have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK")
log.Printf("DHCP: received message from %s: ACK", p.CHAddr())
case dhcp4.NAK: // From Server, No you cannot have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK")
log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
default:
log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType)
log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr())
return nil
}
return nil
}
// Send ICMP to the specified machine
// Return TRUE if it doesn't reply, which probably means that the IP is available
func (s *Server) addrAvailable(target net.IP) bool {
if s.ICMPTimeout == 0 {
return true
}
pinger, err := ping.NewPinger(target.String())
if err != nil {
log.Error("ping.NewPinger(): %v", err)
return true
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
// log.Tracef("Received ICMP Reply from %v", target)
reply = true
}
log.Tracef("Sending ICMP Echo to %v", target)
pinger.Run()
if reply {
log.Info("DHCP: IP conflict: %v is already used by another device", target)
return false
}
log.Tracef("ICMP procedure is complete: %v", target)
return true
}
// Add the specified IP to the black list for a time period
func (s *Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.reserveIP(lease.IP, hw)
s.Lock()
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.leaseTime)
s.Unlock()
}
// Return TRUE if DHCP packet is correct
func isValidPacket(p dhcp4.Packet) bool {
hw := p.CHAddr()
zeroes := make([]byte, len(hw))
if bytes.Equal(hw, zeroes) {
log.Tracef("Packet has empty CHAddr")
return false
}
return true
}
func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
// find a lease, but don't update lease time
var lease *Lease
var err error
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
hostname := p.ParseOptions()[dhcp4.OptionHostName]
log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s",
reqIP, p.CHAddr(), hostname)
if !isValidPacket(p) {
return nil
}
lease = s.findLease(p)
for lease == nil {
lease, err = s.reserveLease(p)
if err != nil {
log.Error("Couldn't find free lease: %s", err)
return nil
}
if !s.addrAvailable(lease.IP) {
s.blacklistLease(lease)
lease = nil
continue
}
break
}
opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt)
log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions())
return reply
}
func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got from client: Request")
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
var lease *Lease
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s",
p.CIAddr(), reqIP, p.CHAddr())
if !isValidPacket(p) {
return nil
}
server := options[dhcp4.OptionServerIdentifier]
if server != nil && !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 = p.CIAddr()
}
if reqIP.To4() == nil {
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
} else if reqIP == nil || reqIP.To4() == nil {
log.Tracef("Requested IP isn't a 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")
lease = s.findLease(p)
if lease == nil {
log.Tracef("Lease for %s isn't found", p.CHAddr())
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) {
log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s",
lease.HWAddr, lease.IP, reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, 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]))
}
lease.Expiry = time.Now().Add(s.leaseTime)
log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s",
lease.IP, lease.HWAddr, lease.Expiry)
opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt)
}
//
// requested IP different from lease
//
func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Message from client: Inform. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
return nil
}
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]))
func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Message from client: Release. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
return nil
}
func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Decline. IP: %s HW: %s",
reqIP, p.CHAddr())
return nil
}
// Leases returns the list of current DHCP leases (thread-safe)
func (s *Server) Leases() []Lease {
var result []Lease
now := time.Now().Unix()
s.RLock()
for _, lease := range s.leases {
if lease.Expiry.Unix() > now {
result = append(result, *lease)
}
}
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)
}
// Leases returns the list of current DHCP leases
func (s *Server) Leases() []*Lease {
s.RLock()
result := s.leases
s.RUnlock()
return result
}
// Print information about the current leases
func (s *Server) printLeases() {
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)
}
}
// Reset internal state
func (s *Server) reset() {
s.Lock()
s.leases = nil
s.Unlock()
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}

170
dhcpd/dhcpd_test.go Normal file
View File

@@ -0,0 +1,170 @@
package dhcpd
import (
"bytes"
"net"
"os"
"testing"
"time"
"github.com/krolaw/dhcp4"
)
func check(t *testing.T, result bool, msg string) {
if !result {
t.Fatal(msg)
}
}
// Tests performed:
// . Handle Discover message (lease reserve)
// . Handle Request message (lease commit)
func TestDHCP(t *testing.T) {
var s = Server{}
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var lease *Lease
var opt dhcp4.Options
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
s.leaseOptions = dhcp4.Options{}
s.ipnet = &net.IPNet{
IP: []byte{1, 2, 3, 4},
Mask: []byte{0xff, 0xff, 0xff, 0xff},
}
p = make(dhcp4.Packet, 241)
// Reserve an IP
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
lease = s.findLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Reserve an IP - the next IP from the range
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 2}), "lease.IP")
// Reserve an IP - we have no more available IPs,
// so the first expired (or, in our case, not yet committed) lease is returned
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Decline request for a lease which doesn't match our internal state
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
// ask a different IP
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
// Commit the previously reserved lease
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK")
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr")
check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr")
check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime")
check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier")
// Commit the previously reserved lease #2
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 2}), "p2.YIAddr")
// Reserve an IP - we have no more available IPs
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, lease == nil, "lease == nil")
s.reset()
misc(t, &s)
}
// Small tests that don't require a static server's state
func misc(t *testing.T, s *Server) {
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var opt dhcp4.Options
p = make(dhcp4.Packet, 241)
// Try to commit a lease for an IP without prior Discover-Offer packets
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
}
// Leases database store/load
func TestDB(t *testing.T) {
var s = Server{}
var p dhcp4.Packet
var hw1, hw2 net.HardwareAddr
var lease *Lease
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
s.leaseOptions = dhcp4.Options{}
s.ipnet = &net.IPNet{
IP: []byte{1, 2, 3, 4},
Mask: []byte{0xff, 0xff, 0xff, 0xff},
}
p = make(dhcp4.Packet, 241)
hw1 = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw1)
lease, _ = s.reserveLease(p)
lease.Expiry = time.Unix(4000000001, 0)
hw2 = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw2)
lease, _ = s.reserveLease(p)
lease.Expiry = time.Unix(4000000002, 0)
os.Remove("leases.db")
s.dbStore()
s.reset()
s.dbLoad()
check(t, bytes.Equal(s.leases[0].HWAddr, hw1), "leases[0].HWAddr")
check(t, bytes.Equal(s.leases[0].IP, []byte{1, 1, 1, 1}), "leases[0].IP")
check(t, s.leases[0].Expiry.Unix() == 4000000001, "leases[0].Expiry")
check(t, bytes.Equal(s.leases[1].HWAddr, hw2), "leases[1].HWAddr")
check(t, bytes.Equal(s.leases[1].IP, []byte{1, 1, 1, 2}), "leases[1].IP")
check(t, s.leases[1].Expiry.Unix() == 4000000002, "leases[1].Expiry")
os.Remove("leases.db")
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"net"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)

45
dhcpd/os_linux.go Normal file
View File

@@ -0,0 +1,45 @@
package dhcpd
import (
"net"
"os"
"syscall"
"golang.org/x/net/ipv4"
)
// Create a socket for receiving broadcast packets
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
if err != nil {
return nil, err
}
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil {
return nil, err
}
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
return nil, err
}
if err := syscall.SetsockoptString(s, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, ifname); err != nil {
return nil, err
}
addr := syscall.SockaddrInet4{Port: port}
copy(addr.Addr[:], bindAddr.To4())
err = syscall.Bind(s, &addr)
if err != nil {
syscall.Close(s)
return nil, err
}
f := os.NewFile(uintptr(s), "")
c, err := net.FilePacketConn(f)
f.Close()
if err != nil {
return nil, err
}
p := ipv4.NewPacketConn(c)
return p, nil
}

44
dhcpd/os_unix.go Normal file
View File

@@ -0,0 +1,44 @@
// +build aix darwin dragonfly freebsd netbsd openbsd solaris
package dhcpd
import (
"net"
"os"
"syscall"
"golang.org/x/net/ipv4"
)
// Create a socket for receiving broadcast packets
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
if err != nil {
return nil, err
}
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil {
return nil, err
}
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
return nil, err
}
addr := syscall.SockaddrInet4{Port: port}
copy(addr.Addr[:], bindAddr.To4())
err = syscall.Bind(s, &addr)
if err != nil {
syscall.Close(s)
return nil, err
}
f := os.NewFile(uintptr(s), "")
c, err := net.FilePacketConn(f)
f.Close()
if err != nil {
return nil, err
}
p := ipv4.NewPacketConn(c)
return p, nil
}

13
dhcpd/os_windows.go Normal file
View File

@@ -0,0 +1,13 @@
package dhcpd
import (
"errors"
"net"
"golang.org/x/net/ipv4"
)
// Create a socket for receiving broadcast packets
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
return nil, errors.New("newBroadcastPacketConn(): not supported on Windows")
}

View File

@@ -8,7 +8,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
)

27
dns.go
View File

@@ -7,8 +7,8 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
@@ -50,6 +50,11 @@ func generateServerConfig() dnsforward.ServerConfig {
FilteringConfig: config.DNS.FilteringConfig,
Filters: filters,
}
bindhost := config.DNS.BindHost
if config.DNS.BindHost == "0.0.0.0" {
bindhost = "127.0.0.1"
}
newconfig.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
if config.TLS.Enabled {
newconfig.TLSConfig = config.TLS.TLSConfig
@@ -58,19 +63,13 @@ func generateServerConfig() dnsforward.ServerConfig {
}
}
for _, u := range config.DNS.UpstreamDNS {
opts := upstream.Options{
Timeout: dnsforward.DefaultTimeout,
Bootstrap: []string{config.DNS.BootstrapDNS},
}
dnsUpstream, err := upstream.AddressToUpstream(u, opts)
if err != nil {
log.Printf("Couldn't get upstream: %s", err)
// continue, just ignore the upstream
continue
}
newconfig.Upstreams = append(newconfig.Upstreams, dnsUpstream)
upstreamConfig, err := proxy.ParseUpstreamsConfig(config.DNS.UpstreamDNS, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
if err != nil {
log.Error("Couldn't get upstreams configuration cause: %s", err)
}
newconfig.Upstreams = upstreamConfig.Upstreams
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
newconfig.AllServers = config.DNS.AllServers
return newconfig
}

View File

@@ -3,6 +3,7 @@ package dnsfilter
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
@@ -16,8 +17,9 @@ import (
"sync/atomic"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"golang.org/x/net/publicsuffix"
)
@@ -45,10 +47,11 @@ const enableDelayedCompilation = true // flag for debugging, must be true in pro
// 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"`
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"`
ResolverAddress string // DNS server address
}
type privateConfig struct {
@@ -180,6 +183,10 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
return Result{Reason: NotFilteredNotFound}, nil
}
host = strings.ToLower(host)
// prevent recursion
if host == d.parentalServer || host == d.safeBrowsingServer {
return Result{}, nil
}
// try filter lists first
result, err := d.matchHost(host)
@@ -600,6 +607,11 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
}
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeSearch HTTP lookup for %s", host)
}
if safeSearchCache == nil {
safeSearchCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
@@ -608,6 +620,7 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
cachedValue, isFound, err := getCachedReason(safeSearchCache, host)
if isFound {
atomic.AddUint64(&stats.Safesearch.CacheHits, 1)
log.Tracef("%s: found in SafeSearch cache", host)
return cachedValue, nil
}
@@ -638,15 +651,17 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
return Result{}, err
}
res.IP = addrs[0]
// The next bug may occurs: LookupIP returns DNS64 mapped ipv4 address with zero-prefix
for _, i := range addrs {
if ipv4 := i.To4(); ipv4 != nil && len(i) == net.IPv6len {
if ipv4 := i.To4(); ipv4 != nil {
res.IP = ipv4
break
}
}
if len(res.IP) == 0 {
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
}
// Cache result
err = safeSearchCache.Set(host, res)
if err != nil {
@@ -656,10 +671,11 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
}
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
// prevent recursion -- checking the host of safebrowsing server makes no sense
if host == d.safeBrowsingServer {
return Result{}, nil
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeBrowsing HTTP lookup for %s", host)
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultSafebrowsingURL, d.safeBrowsingServer, hashparam)
return url
@@ -697,10 +713,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.parentalServer {
return Result{}, nil
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("Parental HTTP lookup for %s", host)
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, d.parentalServer, hashparam, d.ParentalSensitivity)
return url
@@ -754,6 +771,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
cachedValue, isFound, err := getCachedReason(cache, host)
if isFound {
atomic.AddUint64(&lookupstats.CacheHits, 1)
log.Tracef("%s: found in the lookup cache", host)
return cachedValue, nil
}
if err != nil {
@@ -954,6 +972,51 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
// lifecycle helper functions
//
type dialFunctionType func(ctx context.Context, network, addr string) (net.Conn, error)
// Connect to a remote server resolving hostname using our own DNS server
func createCustomDialContext(resolverAddr string) dialFunctionType {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
log.Tracef("network:%v addr:%v", network, addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil {
con, err := dialer.DialContext(ctx, network, addr)
return con, err
}
r := upstream.NewResolver(resolverAddr, 30*time.Second)
addrs, e := r.LookupIPAddr(ctx, host)
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
if e != nil {
return nil, e
}
var firstErr error
firstErr = nil
for _, a := range addrs {
addr = fmt.Sprintf("%s:%s", a.String(), port)
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
return con, err
}
return nil, firstErr
}
}
// New creates properly initialized DNS Filter that is ready to be used
func New(c *Config) *Dnsfilter {
d := new(Dnsfilter)
@@ -973,6 +1036,9 @@ func New(c *Config) *Dnsfilter {
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
if c != nil && len(c.ResolverAddress) != 0 {
d.transport.DialContext = createCustomDialContext(c.ResolverAddress)
}
d.client = http.Client{
Transport: d.transport,
Timeout: defaultHTTPTimeout,

View File

@@ -18,7 +18,7 @@ import (
"os"
"runtime"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/shirou/gopsutil/process"
"go.uber.org/goleak"
)

View File

@@ -14,6 +14,10 @@ var safeSearchDomains = map[string]string{
"www.bing.com": "strict.bing.com",
"duckduckgo.com": "safe.duckduckgo.com",
"www.duckduckgo.com": "safe.duckduckgo.com",
"start.duckduckgo.com": "safe.duckduckgo.com",
"www.google.com": "forcesafesearch.google.com",
"www.google.ad": "forcesafesearch.google.com",
"www.google.ae": "forcesafesearch.google.com",

View File

@@ -13,7 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
"github.com/miekg/dns"
)
@@ -62,11 +62,12 @@ 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"`
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
dnsfilter.Config `yaml:",inline"`
}
@@ -81,10 +82,11 @@ type TLSConfig struct {
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
UDPListenAddr *net.UDPAddr // UDP listen address
TCPListenAddr *net.TCPAddr // TCP listen address
Upstreams []upstream.Upstream // Configured upstreams
Filters []dnsfilter.Filter // A list of filters to use
UDPListenAddr *net.UDPAddr // UDP listen address
TCPListenAddr *net.TCPAddr // TCP listen address
Upstreams []upstream.Upstream // Configured upstreams
DomainsReservedUpstreams map[string][]upstream.Upstream // Map of domains and lists of configured upstreams
Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig
TLSConfig
@@ -155,14 +157,16 @@ func (s *Server) startInternal(config *ServerConfig) error {
})
proxyConfig := proxy.Config{
UDPListenAddr: s.UDPListenAddr,
TCPListenAddr: s.TCPListenAddr,
Ratelimit: s.Ratelimit,
RatelimitWhitelist: s.RatelimitWhitelist,
RefuseAny: s.RefuseAny,
CacheEnabled: true,
Upstreams: s.Upstreams,
Handler: s.handleDNSRequest,
UDPListenAddr: s.UDPListenAddr,
TCPListenAddr: s.TCPListenAddr,
Ratelimit: s.Ratelimit,
RatelimitWhitelist: s.RatelimitWhitelist,
RefuseAny: s.RefuseAny,
CacheEnabled: true,
Upstreams: s.Upstreams,
DomainsReservedUpstreams: s.DomainsReservedUpstreams,
Handler: s.handleDNSRequest,
AllServers: s.AllServers,
}
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
@@ -171,7 +175,10 @@ func (s *Server) startInternal(config *ServerConfig) error {
if err != nil {
return errorx.Decorate(err, "Failed to parse TLS keypair")
}
proxyConfig.TLSConfig = &tls.Config{Certificates: []tls.Certificate{keypair}}
proxyConfig.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{keypair},
MinVersion: tls.VersionTLS12,
}
}
if proxyConfig.UDPListenAddr == nil {

View File

@@ -101,7 +101,11 @@ func TestDotServer(t *testing.T) {
// Add our self-signed generated config to roots
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(certPem)
tlsConfig := &tls.Config{ServerName: tlsServerName, RootCAs: roots}
tlsConfig := &tls.Config{
ServerName: tlsServerName,
RootCAs: roots,
MinVersion: tls.VersionTLS12,
}
// Create a DNS-over-TLS client connection
addr := s.dnsProxy.Addr(proxy.ProtoTLS)
@@ -459,7 +463,7 @@ func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
t.Fatalf("failed to create certificate: %s", err)
}
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName}, certPem, keyPem
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName, MinVersion: tls.VersionTLS12}, certPem, keyPem
}
func createDataDir(t *testing.T) string {

View File

@@ -10,7 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)

View File

@@ -9,8 +9,8 @@ import (
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/go-test/deep"
"github.com/hmage/golibs/log"
)
var (
@@ -28,7 +28,7 @@ func (l *queryLog) flushLogBuffer() error {
l.logBufferLock.Unlock()
err := l.flushToFile(flushBuffer)
if err != nil {
log.Printf("Saving querylog to file failed: %s", err)
log.Error("Saving querylog to file failed: %s", err)
return err
}
return nil
@@ -46,17 +46,17 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
for _, entry := range buffer {
err := e.Encode(entry)
if err != nil {
log.Printf("Failed to marshal entry: %s", err)
log.Error("Failed to marshal entry: %s", err)
return err
}
}
elapsed := time.Since(start)
log.Printf("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
log.Debug("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
err := checkBuffer(buffer, b)
if err != nil {
log.Printf("failed to check buffer: %s", err)
log.Error("failed to check buffer: %s", err)
return err
}
@@ -73,13 +73,13 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
_, err = zw.Write(b.Bytes())
if err != nil {
log.Printf("Couldn't compress to gzip: %s", err)
log.Error("Couldn't compress to gzip: %s", err)
zw.Close()
return err
}
if err = zw.Close(); err != nil {
log.Printf("Couldn't close gzip writer: %s", err)
log.Error("Couldn't close gzip writer: %s", err)
return err
}
} else {
@@ -90,18 +90,18 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
defer fileWriteLock.Unlock()
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Printf("failed to create file \"%s\": %s", filename, err)
log.Error("failed to create file \"%s\": %s", filename, err)
return err
}
defer f.Close()
n, err := f.Write(zb.Bytes())
if err != nil {
log.Printf("Couldn't write to file: %s", err)
log.Error("Couldn't write to file: %s", err)
return err
}
log.Printf("ok \"%s\": %v bytes written", filename, n)
log.Debug("ok \"%s\": %v bytes written", filename, n)
return nil
}
@@ -115,21 +115,21 @@ func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
entry := &logEntry{}
err := d.Decode(entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
log.Error("Failed to decode: %s", err)
return err
}
if diff := deep.Equal(entry, buffer[i]); diff != nil {
log.Printf("decoded buffer differs: %s", diff)
log.Error("decoded buffer differs: %s", diff)
return fmt.Errorf("decoded buffer differs: %s", diff)
}
i++
}
if i != l {
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
log.Print(err)
log.Error("%v", err)
return err
}
log.Printf("check ok: %d entries", i)
log.Debug("check ok: %d entries", i)
return nil
}
@@ -150,11 +150,11 @@ func (l *queryLog) rotateQueryLog() error {
err := os.Rename(from, to)
if err != nil {
log.Printf("Failed to rename querylog: %s", err)
log.Error("Failed to rename querylog: %s", err)
return err
}
log.Printf("Rotated from %s to %s successfully", from, to)
log.Debug("Rotated from %s to %s successfully", from, to)
return nil
}
@@ -163,7 +163,7 @@ func (l *queryLog) periodicQueryLogRotate() {
for range time.Tick(queryLogRotationPeriod) {
err := l.rotateQueryLog()
if err != nil {
log.Printf("Failed to rotate querylog: %s", err)
log.Error("Failed to rotate querylog: %s", err)
// do nothing, continue rotating
}
}
@@ -198,7 +198,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
f, err := os.Open(file)
if err != nil {
log.Printf("Failed to open file \"%s\": %s", file, err)
log.Error("Failed to open file \"%s\": %s", file, err)
// try next file
continue
}
@@ -209,7 +209,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
if enableGzip {
zr, err := gzip.NewReader(f)
if err != nil {
log.Printf("Failed to create gzip reader: %s", err)
log.Error("Failed to create gzip reader: %s", err)
continue
}
defer zr.Close()
@@ -231,7 +231,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
var entry logEntry
err := d.Decode(&entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
log.Error("Failed to decode: %s", err)
// next entry can be fine, try more
continue
}
@@ -260,7 +260,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
perunit = elapsed / time.Duration(i)
avg = sum / time.Duration(i)
}
log.Printf("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
log.Debug("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
}
return nil
}

View File

@@ -9,8 +9,8 @@ import (
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"github.com/miekg/dns"
)
@@ -158,6 +158,11 @@ func (d *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
// if question hostname is empty, do nothing
if hostname == "" {
return nil
}
// get value, if not set, crate one
d.hoursReadLock()
defer d.hoursReadUnlock()

229
filter.go
View File

@@ -2,17 +2,18 @@ package main
import (
"fmt"
"hash/crc32"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
)
var (
@@ -27,6 +28,7 @@ type filter struct {
Name string `json:"name" yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"`
checksum uint32 // checksum of the file data
dnsfilter.Filter `yaml:",inline"`
}
@@ -42,6 +44,88 @@ func userFilter() filter {
}
}
// Enable or disable a filter
func filterEnable(url string, enable bool) bool {
r := false
config.Lock()
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
if filter.URL == url {
filter.Enabled = enable
if enable {
e := filter.load()
if e != nil {
// This isn't a fatal error,
// because it may occur when someone removes the file from disk.
// In this case the periodic update task will try to download the file.
filter.LastUpdated = time.Time{}
log.Tracef("%s filter load: %v", url, e)
}
} else {
filter.unload()
}
r = true
break
}
}
config.Unlock()
return r
}
// Return TRUE if a filter with this URL exists
func filterExists(url string) bool {
r := false
config.RLock()
for i := range config.Filters {
if config.Filters[i].URL == url {
r = true
break
}
}
config.RUnlock()
return r
}
// Add a filter
// Return FALSE if a filter with this URL exists
func filterAdd(f filter) bool {
config.Lock()
// Check for duplicates
for i := range config.Filters {
if config.Filters[i].URL == f.URL {
config.Unlock()
return false
}
}
config.Filters = append(config.Filters, f)
config.Unlock()
return true
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
func loadFilters() {
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
if !filter.Enabled {
// No need to load a filter that is not enabled
continue
}
err := filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err)
}
}
}
func deduplicateFilters() {
// Deduplicate filters
i := 0 // output index, used for deletion later
@@ -83,41 +167,88 @@ func periodicallyRefreshFilters() {
// Checks filters updates if necessary
// If force is true, it ignores the filter.LastUpdated field value
//
// Algorithm:
// . Get the list of filters to be updated
// . For each filter run the download and checksum check operation
// . If filter data hasn't changed, set new update time
// . If filter data has changed, parse it, save it on disk, set new update time
// . Apply changes to the current configuration
// . Restart server
func refreshFiltersIfNecessary(force bool) int {
config.Lock()
var updateFilters []filter
// fetch URLs
updateCount := 0
if config.firstRun {
return 0
}
config.RLock()
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
f := &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()
if !f.Enabled {
continue
}
if len(filter.Rules) == 0 {
// Try reloading filter from the disk before updating
// This is useful for the case when we simply enable a previously downloaded filter
_ = filter.load()
if !force && time.Since(f.LastUpdated) <= updatePeriod {
continue
}
updated, err := filter.update(force)
var uf filter
uf.ID = f.ID
uf.URL = f.URL
uf.Name = f.Name
uf.checksum = f.checksum
updateFilters = append(updateFilters, uf)
}
config.RUnlock()
updateCount := 0
for i := range updateFilters {
uf := &updateFilters[i]
updated, err := uf.update()
if err != nil {
log.Printf("Failed to update filter %s: %s\n", filter.URL, err)
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
continue
}
if updated {
// Saving it to the filters dir now
err = filter.save()
err = uf.save()
if err != nil {
log.Printf("Failed to save the updated filter %d: %s", filter.ID, err)
log.Printf("Failed to save the updated filter %d: %s", uf.ID, err)
continue
}
} else {
mtime := time.Now()
e := os.Chtimes(uf.Path(), mtime, mtime)
if e != nil {
log.Error("os.Chtimes(): %v", e)
}
uf.LastUpdated = mtime
}
config.Lock()
for k := range config.Filters {
f := &config.Filters[k]
if f.ID != uf.ID || f.URL != uf.URL {
continue
}
f.LastUpdated = uf.LastUpdated
if !updated {
continue
}
log.Info("Updated filter #%d. Rules: %d -> %d",
f.ID, f.RulesCount, uf.RulesCount)
f.Name = uf.Name
f.Rules = uf.Rules
f.RulesCount = uf.RulesCount
f.checksum = uf.checksum
updateCount++
}
config.Unlock()
}
config.Unlock()
if updateCount > 0 && isRunning() {
err := reconfigureDNSServer()
@@ -138,13 +269,19 @@ func parseFilterContents(contents []byte) (int, string, []string) {
// 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 {
if len(line) == 0 {
continue
}
if line[0] == '!' {
m := filterTitleRegexp.FindAllStringSubmatch(line, -1)
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1]
seenTitle = true
}
} else if len(line) != 0 {
} else {
rulesCount++
}
}
@@ -152,20 +289,8 @@ func parseFilterContents(contents []byte) (int, string, []string) {
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
}
// Perform upgrade on a filter
func (filter *filter) update() (bool, error) {
log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
resp, err := client.Get(filter.URL)
@@ -194,22 +319,22 @@ func (filter *filter) update(force bool) (bool, error) {
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) {
checksum := crc32.ChecksumIEEE(body)
if filter.checksum == checksum {
log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil
}
// Extract filter name and count number of rules
rulesCount, filterName, rules := parseFilterContents(body)
log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount)
if filterName != "" {
filter.Name = filterName
}
filter.RulesCount = rulesCount
filter.Rules = rules
filter.checksum = checksum
return true, nil
}
@@ -220,7 +345,7 @@ func (filter *filter) save() error {
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
body := []byte(strings.Join(filter.Rules, "\n"))
err := safeWriteFile(filterFilePath, body)
err := file.SafeWrite(filterFilePath, body)
// update LastUpdated field after saving the file
filter.LastUpdated = filter.LastTimeUpdated()
@@ -229,11 +354,6 @@ func (filter *filter) save() error {
// 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.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath)
@@ -252,25 +372,32 @@ func (filter *filter) load() error {
filter.RulesCount = rulesCount
filter.Rules = rules
filter.checksum = crc32.ChecksumIEEE(filterFileContents)
filter.LastUpdated = filter.LastTimeUpdated()
return nil
}
// Clear filter rules
func (filter *filter) unload() {
filter.Rules = []string{}
filter.RulesCount = 0
}
// Path to the filter contents
func (filter *filter) Path() string {
return filepath.Join(config.ourWorkingDir, dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
}
// LastUpdated returns the time when the filter was last time updated
// LastTimeUpdated returns the time when the filter was last time updated
func (filter *filter) LastTimeUpdated() time.Time {
filterFilePath := filter.Path()
if _, err := os.Stat(filterFilePath); os.IsNotExist(err) {
s, err := os.Stat(filterFilePath)
if os.IsNotExist(err) {
// if the filter file does not exist, return 0001-01-01
return time.Time{}
}
s, err := os.Stat(filterFilePath)
if err != nil {
// if the filter file does not exist, return 0001-01-01
return time.Time{}

12
go.mod
View File

@@ -1,13 +1,16 @@
module github.com/AdguardTeam/AdGuardHome
go 1.12
require (
github.com/AdguardTeam/dnsproxy v0.11.1
github.com/AdguardTeam/dnsproxy v0.12.0
github.com/AdguardTeam/golibs v0.1.3
github.com/NYTimes/gziphandler v1.1.1
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0
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/hmage/golibs v0.0.0-20190121112702-20153bd03c24
github.com/joomcode/errorx v0.1.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
@@ -15,7 +18,8 @@ require (
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
github.com/stretchr/testify v1.2.2
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
github.com/stretchr/testify v1.3.0
go.uber.org/goleak v0.10.0
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
golang.org/x/sys v0.0.0-20190122071731-054c452bb702

21
go.sum
View File

@@ -1,5 +1,10 @@
github.com/AdguardTeam/dnsproxy v0.11.1 h1:qO5VH0GYF9vdksQRG8frEfJ+CJjsPBwuct8FH6Mij7o=
github.com/AdguardTeam/dnsproxy v0.11.1/go.mod h1:lEi2srAWwfSQWoy8GeZR6lwS+FSMoiZid8bQPreOLb0=
github.com/AdguardTeam/dnsproxy v0.12.0 h1:BPgv2PlH2u4xakFcaW4EqU3Visk1BNidrqGSgxe5Qzg=
github.com/AdguardTeam/dnsproxy v0.12.0/go.mod h1:lcZM2QPwcWGEL3pz8RYy06nQdbjj4pr+94H45jnVSHg=
github.com/AdguardTeam/golibs v0.1.2/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
github.com/AdguardTeam/golibs v0.1.3 h1:hmapdTtMtIk3T8eQDwTOLdqZLGDKNKk9325uC8z12xg=
github.com/AdguardTeam/golibs v0.1.3/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
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=
@@ -12,8 +17,9 @@ github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4Jhn
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/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/bluele/gcache v0.0.0-20190203144525-2016d595ccb0 h1:vUdUwmQLnT/yuk8PsDhhMVkrfr4aMdcv/0GWzIqOjEY=
github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
@@ -26,8 +32,6 @@ 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/hmage/golibs v0.0.0-20190121112702-20153bd03c24 h1:yyDtaSMcAZdm1I6uL8YLghpWiJljfBHs8NC/P86PYQk=
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24/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=
@@ -55,12 +59,17 @@ github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBh
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=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=

View File

@@ -2,10 +2,10 @@ package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -15,30 +15,14 @@ import (
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
// ----------------------------------
// helper functions for working with files
// ----------------------------------
// Writes data first to a temporary file and then renames it to what's specified in path
func safeWriteFile(path string, data []byte) error {
dir := filepath.Dir(path)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
tmpPath := path + ".tmp"
err = ioutil.WriteFile(tmpPath, data, 0644)
if err != nil {
return err
}
return os.Rename(tmpPath, path)
}
// ----------------------------------
// helper functions for HTTP handlers
// ----------------------------------
@@ -48,6 +32,12 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return
}
if method == "POST" || method == "PUT" || method == "DELETE" {
controlLock.Lock()
defer controlLock.Unlock()
}
handler(w, r)
}
}
@@ -60,12 +50,19 @@ func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.Respo
return ensure("GET", handler)
}
func ensurePUT(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("PUT", handler)
// Bridge between http.Handler object and Go function
type httpHandler struct {
handler func(http.ResponseWriter, *http.Request)
}
func ensureDELETE(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("DELETE", handler)
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func ensureGETHandler(handler func(http.ResponseWriter, *http.Request)) http.Handler {
h := httpHandler{}
h.handler = ensure("GET", handler)
return &h
}
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
@@ -308,6 +305,77 @@ func checkPacketPortAvailable(host string, port int) error {
return err
}
// Connect to a remote server resolving hostname using our own DNS server
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
log.Tracef("network:%v addr:%v", network, addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil {
con, err := dialer.DialContext(ctx, network, addr)
return con, err
}
bindhost := config.DNS.BindHost
if config.DNS.BindHost == "0.0.0.0" {
bindhost = "127.0.0.1"
}
resolverAddr := fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
r := upstream.NewResolver(resolverAddr, 30*time.Second)
addrs, e := r.LookupIPAddr(ctx, host)
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
if e != nil {
return nil, e
}
var firstErr error
firstErr = nil
for _, a := range addrs {
addr = fmt.Sprintf("%s:%s", a.String(), port)
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
return con, err
}
return nil, firstErr
}
// check if error is "address already in use"
func errorIsAddrInUse(err error) bool {
errOpError, ok := err.(*net.OpError)
if !ok {
return false
}
errSyscallError, ok := errOpError.Err.(*os.SyscallError)
if !ok {
return false
}
errErrno, ok := errSyscallError.Err.(syscall.Errno)
if !ok {
return false
}
if runtime.GOOS == "windows" {
const WSAEADDRINUSE = 10048
return errErrno == WSAEADDRINUSE
}
return errErrno == syscall.EADDRINUSE
}
// ---------------------
// debug logging helpers
// ---------------------

View File

@@ -3,7 +3,7 @@ package main
import (
"testing"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
)
func TestGetValidNetInterfacesForWeb(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"strings"
"github.com/hmage/golibs/log"
"github.com/AdguardTeam/golibs/log"
)
// --------------------
@@ -22,6 +22,8 @@ var allowedLanguages = map[string]bool{
"sv": true,
"pt-br": true,
"zh-tw": true,
"bg": true,
"zh-cn": true,
}
func isLanguageAllowed(language string) bool {

View File

@@ -2,7 +2,7 @@ swagger: '2.0'
info:
title: 'AdGuard Home'
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
version: 0.93.0
version: 0.95.0
schemes:
- http
basePath: /control
@@ -39,6 +39,9 @@ tags:
-
name: dhcp
description: 'Built-in DHCP server controls'
-
name: clients
description: 'Clients list operations'
-
name: install
description: 'First-time install configuration handlers'
@@ -87,26 +90,21 @@ paths:
200:
description: OK
/set_upstream_dns:
/set_upstreams_config:
post:
tags:
- global
operationId: setUpstreamDNS
summary: 'Set upstream DNS for coredns, empty value will reset it to default values'
operationId: setUpstreamsConfig
summary: "Updates the current upstreams configuration"
consumes:
- text/plain
- application/json
parameters:
- in: body
name: upstream
description: 'Upstream servers, separated by newline or space, port is optional after colon'
schema:
# TODO: use JSON
type: string
example: |
1.1.1.1
1.0.0.1
8.8.8.8 8.8.4.4
192.168.1.104:53535
- in: "body"
name: "body"
description: "Upstreams configuration JSON"
required: true
schema:
$ref: "#/definitions/UpstreamsConfig"
responses:
200:
description: OK
@@ -116,21 +114,15 @@ paths:
tags:
- global
operationId: testUpstreamDNS
summary: 'Test upstream DNS'
summary: "Test upstream configuration"
consumes:
- text/plain
- application/json
parameters:
- in: body
name: upstream
description: 'Upstream servers, separated by newline or space, port is optional after colon'
- in: "body"
name: "body"
description: "Upstream configuration to be tested"
schema:
# TODO: use JSON
type: string
example: |
1.1.1.1
1.0.0.1
8.8.8.8 8.8.4.4
192.168.1.104:53535
$ref: "#/definitions/UpstreamsConfig"
responses:
200:
description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.'
@@ -679,6 +671,22 @@ paths:
application/json:
enabled: false
# --------------------------------------------------
# Clients list methods
# --------------------------------------------------
/clients:
get:
tags:
- clients
operationId: clientsStatus
summary: 'Get information about configured clients'
responses:
200:
description: OK
schema:
$ref: "#/definitions/Clients"
# --------------------------------------------------
# I18N methods
# --------------------------------------------------
@@ -731,6 +739,26 @@ paths:
description: OK
schema:
$ref: "#/definitions/AddressesInfo"
/install/check_config:
post:
tags:
- install
operationId: installCheckConfig
summary: "Checks configuration"
parameters:
- in: "body"
name: "body"
description: "Configuration to be checked"
required: true
schema:
$ref: "#/definitions/CheckConfigRequest"
responses:
200:
description: OK
schema:
$ref: "#/definitions/CheckConfigResponse"
400:
description: "Failed to parse JSON or cannot listen on the specified address"
/install/configure:
post:
tags:
@@ -798,6 +826,33 @@ definitions:
language:
type: "string"
example: "en"
UpstreamsConfig:
type: "object"
description: "Upstreams configuration"
required:
- "bootstrap_dns"
- "upstream_dns"
- "all_servers"
properties:
bootstrap_dns:
type: "array"
description: 'Bootstrap servers, port is optional after colon. Empty value will reset it to default values'
items:
type: "string"
example:
- "8.8.8.8:53"
- "1.1.1.1:53"
upstream_dns:
type: "array"
description: 'Upstream servers, port is optional after colon. Empty value will reset it to default values'
items:
type: "string"
example:
- "tls://1.1.1.1"
- "tls://1.0.0.1"
all_servers:
type: "boolean"
description: "If true, parallel queries to all configured upstream servers are enabled"
Filter:
type: "object"
description: "Filter subscription info"
@@ -1087,14 +1142,33 @@ definitions:
DhcpSearchResult:
type: "object"
description: "Information about a DHCP server discovered in the current network"
required:
- "found"
properties:
other_server:
$ref: "#/definitions/DhcpSearchResultOtherServer"
static_ip:
$ref: "#/definitions/DhcpSearchResultStaticIP"
DhcpSearchResultOtherServer:
type: "object"
properties:
found:
type: "boolean"
gateway_ip:
type: "string"
example: "192.168.1.1"
description: "yes|no|error"
example: "no"
error:
type: "string"
description: "Set if found=error"
example: ""
DhcpSearchResultStaticIP:
type: "object"
properties:
static:
type: "string"
description: "yes|no|error"
example: "yes"
ip:
type: "string"
description: "Set if static=no"
example: ""
DnsAnswer:
type: "object"
description: "DNS answer section"
@@ -1247,6 +1321,10 @@ definitions:
type: "string"
example: "You have specified an empty certificate"
description: "warning_validation is a validation warning message with the issue description"
valid_pair:
type: "boolean"
example: "true"
description: "valid_pair is true if both certificate and private key are correct"
NetInterface:
type: "object"
description: "Network interface info"
@@ -1276,27 +1354,82 @@ definitions:
properties:
ip:
type: "string"
example: "127.0.01"
example: "127.0.0.1"
port:
type: "integer"
format: "int32"
example: 53
warning:
type: "string"
example: "Cannot bind to this port"
AddressesInfo:
type: "object"
description: "AdGuard Home addresses configuration"
properties:
dns:
$ref: "#/definitions/AddressInfo"
web:
$ref: "#/definitions/AddressInfo"
dns_port:
type: "integer"
format: "int32"
example: 53
web_port:
type: "integer"
format: "int32"
example: 80
interfaces:
type: "object"
description: "Network interfaces dictionary (key is the interface name)"
additionalProperties:
$ref: "#/definitions/NetInterface"
Client:
type: "object"
description: "Client information"
properties:
ip:
type: "string"
description: "IP address"
example: "127.0.0.1"
name:
type: "string"
description: "Name"
example: "localhost"
Clients:
type: "array"
items:
$ref: "#/definitions/Client"
description: "Clients array"
CheckConfigRequest:
type: "object"
description: "Configuration to be checked"
properties:
dns:
$ref: "#/definitions/CheckConfigRequestInfo"
web:
$ref: "#/definitions/CheckConfigRequestInfo"
CheckConfigRequestInfo:
type: "object"
properties:
ip:
type: "string"
example: "127.0.0.1"
port:
type: "integer"
format: "int32"
example: 53
autofix:
type: "boolean"
example: false
CheckConfigResponse:
type: "object"
properties:
dns:
$ref: "#/definitions/CheckConfigResponseInfo"
web:
$ref: "#/definitions/CheckConfigResponseInfo"
CheckConfigResponseInfo:
type: "object"
properties:
status:
type: "string"
example: ""
can_autofix:
type: "boolean"
example: false
InitialConfiguration:
type: "object"
description: "AdGuard Home initial configuration (for the first-install wizard)"

27
os_unix.go Normal file
View File

@@ -0,0 +1,27 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package main
import (
"os"
"syscall"
"github.com/AdguardTeam/golibs/log"
)
// Set user-specified limit of how many fd's we can use
// https://github.com/AdguardTeam/AdGuardHome/issues/659
func setRlimit(val uint) {
var rlim syscall.Rlimit
rlim.Max = uint64(val)
rlim.Cur = uint64(val)
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
if err != nil {
log.Error("Setrlimit() failed: %v", err)
}
}
// Check if the current user has root (administrator) rights
func haveAdminRights() (bool, error) {
return os.Getuid() == 0, nil
}

Some files were not shown because too many files have changed in this diff Show More