Compare commits

...

255 Commits

Author SHA1 Message Date
Andrey Meshkov
9fe34818e3 Fix #770 - dnsproxy v0.15.0
* commit '9a77bb3a0acddf88f32991e13891270deea5725c':
  go mod tidy
  * dnsproxy v0.15.0
2019-06-07 20:13:59 +03:00
Andrey Meshkov
9a77bb3a0a go mod tidy 2019-06-07 20:10:07 +03:00
Simon Zolin
86890a8609 * dnsproxy v0.15.0 2019-06-07 19:59:11 +03:00
Simon Zolin
1fd0f78612 Merge: - clients: fix race introduced by commit 07db927; update tech doc
Close #727

* commit '1fcb69d3a913dec9b53f148acab45b1f621faa24':
  - clients: fix race introduced by commit 07db927; update tech doc
2019-06-07 19:11:28 +03:00
Simon Zolin
1fcb69d3a9 - clients: fix race introduced by commit 07db927; update tech doc 2019-06-07 11:37:55 +03:00
Andrey Meshkov
07db927246 Fix #727 - use default parental sensitivity when it's not set 2019-06-06 22:42:17 +03:00
Andrey Meshkov
f9807e4011 Fix #806 2019-06-06 21:06:19 +03:00
Andrey Meshkov
5647bc1fc9 Fix #727 - apply client settings properly 2019-06-06 21:04:17 +03:00
Andrey Meshkov
0f7235f217 fix string id 2019-06-06 17:48:46 +03:00
Andrey Meshkov
db67fb6c6a rename client settings 2019-06-06 17:45:24 +03:00
Simon Zolin
087d2f68c2 Merge: - client: fix versions check
Close #428

* commit '02fa39226c442a23e9cba9816382f55c2b276589':
  * client: pull locales
  - client: fix versions check
2019-06-06 17:27:38 +03:00
Ildar Kamalov
02fa39226c * client: pull locales 2019-06-06 17:25:50 +03:00
Andrey Meshkov
edfa104710 add urlfilter to the list of software 2019-06-06 16:18:12 +03:00
Andrey Meshkov
7a124213e5 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-dns 2019-06-06 16:00:42 +03:00
Andrey Meshkov
0a2a7ca630 Fix #566 -- added comparison table 2019-06-06 16:00:35 +03:00
Ildar Kamalov
1f164c7005 - client: fix versions check 2019-06-06 15:54:19 +03:00
Simon Zolin
44f224d69e Merge: Auto-update: improve algorithm, fix bugs
Close #428

* commit '22469bb83bcf902804191d4896c2224e5d5b9f75':
  * control_update_test.go: "+build ignore"
  - client: check version on update before page reload
  * auto-update: remove the update directory after successful update
  + auto-update: copy supporting files (README, etc.)
  * release.sh: add AdGuardHome/ directory to .tar.gz output file
  * auto-update: zipFileUnpack() returns list of unpacked files
  * auto-update: use native code to unpack .tar.gz
  * auto-update: use 'selfupdate_min_version' from version.json
  - control: outgoing HTTP requests didn't work if target IP is IPv6
2019-06-06 14:05:43 +03:00
Simon Zolin
22469bb83b * control_update_test.go: "+build ignore" 2019-06-06 12:20:26 +03:00
Ildar Kamalov
10a0873bc8 - client: check version on update before page reload 2019-06-06 11:41:53 +03:00
Simon Zolin
a165410c9f * auto-update: remove the update directory after successful update 2019-06-06 11:41:53 +03:00
Simon Zolin
ec5e2be31f + auto-update: copy supporting files (README, etc.) 2019-06-06 11:41:53 +03:00
Simon Zolin
6d3099acd3 * release.sh: add AdGuardHome/ directory to .tar.gz output file 2019-06-06 11:41:53 +03:00
Simon Zolin
66c670c6ff * auto-update: zipFileUnpack() returns list of unpacked files 2019-06-06 11:41:14 +03:00
Simon Zolin
c2a31f9503 * auto-update: use native code to unpack .tar.gz 2019-06-06 11:41:14 +03:00
Simon Zolin
466f553bbe * auto-update: use 'selfupdate_min_version' from version.json 2019-06-06 11:41:14 +03:00
Simon Zolin
ddb1bc0fee - control: outgoing HTTP requests didn't work if target IP is IPv6 2019-06-06 11:41:14 +03:00
Andrey Meshkov
a36630e5a8 Merge: Fix #773 - preparing the new update channel
* commit '395833056097fa44c1221da33319cd64e4a5ca62':
  Fix #773 - preparing the new update channel
2019-06-06 11:23:55 +03:00
Andrey Meshkov
3958330560 Fix #773 - preparing the new update channel 2019-06-06 03:00:15 +03:00
Ildar Kamalov
86ba6d4332 Merge pull request in DNS/adguard-dns from fix/793 to master
Closes #793

* commit 'b2364e465f7c7b19ad455b07864ddaf2848e6664':
  * client: reload list on opening Clients settings
2019-06-05 16:06:54 +03:00
Ildar Kamalov
b2364e465f * client: reload list on opening Clients settings 2019-06-05 15:49:01 +03:00
Andrey Meshkov
a3b8d4d923 Fix #706 -- rDNS for DOH/DOT clients 2019-06-04 20:38:53 +03:00
Andrey Meshkov
3454bf9243 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-dns 2019-06-04 19:58:17 +03:00
Andrey Meshkov
64a4443d0c Fix #706 - logging for resolveRDNS 2019-06-04 19:58:13 +03:00
Simon Zolin
d24b78db0e Merge: + dns: resolve hosts via rDNS for the top clients after the server has been started
Close #706

* commit 'f7150e6a19bfc5643fef4106bd068f6ba099f8e2':
  + dns: resolve hosts via rDNS for the top clients after the server has been started
2019-06-04 18:59:14 +03:00
Simon Zolin
f7150e6a19 + dns: resolve hosts via rDNS for the top clients after the server has been started 2019-06-04 18:12:45 +03:00
Andrey Meshkov
85046abb15 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-dns 2019-06-04 16:30:59 +03:00
Andrey Meshkov
454e26db7f Update ports in docker files 2019-06-04 16:30:55 +03:00
Andrey Meshkov
b74438bf83 Merge pull request 235, Fix #778
* commit '7d40d3bfeaa2bea83ca50b41550b16907e447a47':
  + docker: use --no-check-update
2019-06-04 16:28:17 +03:00
Simon Zolin
7d40d3bfea + docker: use --no-check-update 2019-06-04 16:06:00 +03:00
Simon Zolin
6261fb79ab Merge: fix docker build
Close #789

* commit '27bffef9400615f502f4799fb318254b585fad99':
  Fix #789
2019-06-04 11:34:16 +03:00
Eugene Zbiranik
27bffef940 Fix #789 2019-06-04 11:03:13 +03:00
Simon Zolin
450e2ac549 Merge: * minor API changes
Close #785

* commit '7f5ac19b592c2a63e52a5e5319e9e8eb687c7410':
  * client: use JSON for filtering/remove_url
  * /remove_url: use JSON input data format
  - openapi: correct format
  - openapi: fix /add_url
2019-06-03 19:39:40 +03:00
Simon Zolin
6ac466e430 Merge: + client: split settings page into several pages
Close #726

* commit 'f7d88f6976ae8328bc47c0df4686ae6a38ed7bb0':
  * client: check initial access settings
  * client: remove unused addErrorToast method
  * client: move access settings to DNS settings page
  + client: split settings page into several pages
2019-06-03 19:38:21 +03:00
Ildar Kamalov
f7d88f6976 * client: check initial access settings 2019-06-03 16:39:02 +03:00
Ildar Kamalov
7f5ac19b59 * client: use JSON for filtering/remove_url 2019-06-03 16:33:15 +03:00
Ildar Kamalov
54f6710b8f * client: remove unused addErrorToast method 2019-06-03 16:18:49 +03:00
Ildar Kamalov
757bb7285a * client: move access settings to DNS settings page 2019-06-03 16:08:50 +03:00
Ildar Kamalov
af041bcbd7 Merge pull request in DNS/adguard-dns from fix/702 to master
* commit 'df9864ec00fe9b4c7f4912a6122d1308f06fa844':
  * client: replace favicon.ico with favicon.png
2019-06-03 15:47:42 +03:00
Ildar Kamalov
cf53653cfa + client: split settings page into several pages 2019-06-03 15:44:29 +03:00
Simon Zolin
1d09ff0562 Merge: + dnsforward: add access settings for blocking DNS requests
Close #728

* commit 'e4532a27cd2a6f92aaf724fddbffa00fcecb064c':
  - openapi: correct format
  + client: handle access settings
  * go.mod: update dnsproxy
  + control: /access/list, /access/set handlers
  + dnsforward: add access settings for blocking DNS requests
2019-06-03 15:04:52 +03:00
Simon Zolin
c93cb43db8 * /remove_url: use JSON input data format 2019-06-03 12:05:08 +03:00
Simon Zolin
276d87a218 - openapi: correct format 2019-06-03 12:05:08 +03:00
Simon Zolin
fcf609ac1e - openapi: fix /add_url 2019-06-03 12:05:08 +03:00
Simon Zolin
e4532a27cd - openapi: correct format 2019-06-03 11:21:57 +03:00
Simon Zolin
302a11a6a3 Merge: - fix tests and linter issues
* commit 'b8d9ca942c23b37133dbb894d42a8b3f310a86a1':
  - app: fix crash on starting DNS server after installation
  - fix tests and linter issues
2019-05-31 18:49:07 +03:00
Simon Zolin
b8d9ca942c - app: fix crash on starting DNS server after installation 2019-05-31 16:39:18 +03:00
Ildar Kamalov
df9864ec00 * client: replace favicon.ico with favicon.png 2019-05-31 16:36:48 +03:00
Simon Zolin
3baa6919dc - fix tests and linter issues 2019-05-31 12:27:13 +03:00
Ildar Kamalov
02db488b30 + client: handle access settings 2019-05-30 18:45:56 +03:00
Simon Zolin
821ad3edd9 * go.mod: update dnsproxy 2019-05-30 18:22:29 +03:00
Simon Zolin
d18c222b1a + control: /access/list, /access/set handlers 2019-05-30 18:21:56 +03:00
Simon Zolin
36ffcf7d22 + dnsforward: add access settings for blocking DNS requests
Block by client IP or target domain name.
2019-05-30 18:21:36 +03:00
Simon Zolin
147344afa3 Merge: - dns: fix crash (rDNS)
* commit '1abd9da27d7ced46a92e2b0cb85224a1d7d9025b':
  - dns: fix crash (rDNS)
2019-05-29 12:50:04 +03:00
Simon Zolin
1abd9da27d - dns: fix crash (rDNS) 2019-05-28 19:51:49 +03:00
Simon Zolin
d9e70f5244 Merge: + DHCP: Support statically configured leases
Close #687

* commit 'b1fbd7c40c640eef575e6c2babc7eab26a525cf8':
  * openapi: add new dhcp methods
  * client: fix page scrolling on adding/deleting leases
  + client: handle static leases form
  + client: add table to show static leases
  + doc: DHCP static leases
  * dhcpd: refactor: use separate objects for ServerConfig and RWMutex
  + dhcp: /dhcp/status: return static leases
  * dhcpd: minor improvements
  * control: refactor: move DHCP lease -> json convertor to a separate function
  + dhcp: /dhcp/add_static_lease, /dhcp/remove_static_lease: control static lease table
  + helpers: parseIPv4()
  * control: use new DHCP functions: CheckConfig, Init, Start
  * control,dhcp: use dhcpServerConfigJSON struct
  + dhcpd: CheckConfig()
  * dhcpd: move code from Start() to Init()
2019-05-28 19:34:42 +03:00
Simon Zolin
a1ceb83da0 Merge: + clients: find DNS client's hostname by IP using rDNS
Close #706

* commit 'a12f01793ff97e0ea53bc6f751bee758d1df6bb2':
  + clients: find DNS client's hostname by IP using rDNS
2019-05-28 19:33:30 +03:00
Simon Zolin
a12f01793f + clients: find DNS client's hostname by IP using rDNS 2019-05-28 19:07:57 +03:00
Simon Zolin
b1fbd7c40c * openapi: add new dhcp methods 2019-05-28 19:01:24 +03:00
Ildar Kamalov
2976726f99 * client: fix page scrolling on adding/deleting leases 2019-05-28 19:01:24 +03:00
Ildar Kamalov
6f2503a09f + client: handle static leases form 2019-05-28 19:01:24 +03:00
Ildar Kamalov
a8384c004e + client: add table to show static leases 2019-05-28 19:01:24 +03:00
Simon Zolin
49b91b4fc9 + doc: DHCP static leases 2019-05-28 19:01:24 +03:00
Simon Zolin
fa47fa3f9c * dhcpd: refactor: use separate objects for ServerConfig and RWMutex 2019-05-28 19:01:24 +03:00
Simon Zolin
763b986955 + dhcp: /dhcp/status: return static leases 2019-05-28 18:59:15 +03:00
Simon Zolin
342699d933 * dhcpd: minor improvements 2019-05-28 18:59:15 +03:00
Simon Zolin
fd593f5282 * control: refactor: move DHCP lease -> json convertor to a separate function 2019-05-28 18:59:15 +03:00
Simon Zolin
725aeeb910 + dhcp: /dhcp/add_static_lease, /dhcp/remove_static_lease: control static lease table 2019-05-28 18:59:15 +03:00
Simon Zolin
564a41d598 + helpers: parseIPv4() 2019-05-28 18:59:15 +03:00
Simon Zolin
c3204664c3 * control: use new DHCP functions: CheckConfig, Init, Start 2019-05-28 18:59:15 +03:00
Simon Zolin
626c1ae753 * control,dhcp: use dhcpServerConfigJSON struct 2019-05-28 18:59:15 +03:00
Simon Zolin
cc366495d3 + dhcpd: CheckConfig() 2019-05-28 18:59:15 +03:00
Simon Zolin
0d405c0af8 * dhcpd: move code from Start() to Init() 2019-05-28 18:59:15 +03:00
Simon Zolin
c038e4cf14 Merge: + Per-client settings
Close #727

* commit 'a83bc5eeeb4107f2157443b7b40636036fe2a7cc':
  * client: add source column
  * client: remove redundant table formatting for runtime clients table
  * client: show MAC address as default
  + client: add runtime clients table
  * client: add icons for table buttons
  * client: remove unused api method
  * client: confirm before deleting
  * client: remove table column min-width
  * client: fix no data text
  * client: fix sort helper
  + client: handle per-client settings
  - openapi.yaml: fix HTTP methods
  + openapi.yaml: add /clients handlers
  + dnsfilter: use callback function for applying per-client settings
  + dhcp: FindIPbyMAC()
  + dns: use per-client filtering settings
  + clients: config: save/restore clients info array
  + clients API
  + doc: clients
2019-05-28 18:52:51 +03:00
Ildar Kamalov
a83bc5eeeb * client: add source column 2019-05-28 18:44:27 +03:00
Ildar Kamalov
702db84e39 * client: remove redundant table formatting for runtime clients table 2019-05-28 18:44:27 +03:00
Ildar Kamalov
9cc824d852 * client: show MAC address as default 2019-05-28 18:44:27 +03:00
Ildar Kamalov
8a8c7329f7 + client: add runtime clients table 2019-05-28 18:44:27 +03:00
Ildar Kamalov
cbef338592 * client: add icons for table buttons 2019-05-28 18:44:27 +03:00
Ildar Kamalov
bd2c4269db * client: remove unused api method 2019-05-28 18:44:27 +03:00
Ildar Kamalov
f40141bbbc * client: confirm before deleting 2019-05-28 18:44:27 +03:00
Ildar Kamalov
c7b5830336 * client: remove table column min-width 2019-05-28 18:44:27 +03:00
Ildar Kamalov
bb34381a0d * client: fix no data text 2019-05-28 18:44:27 +03:00
Ildar Kamalov
68a4cc597f * client: fix sort helper 2019-05-28 18:44:27 +03:00
Ildar Kamalov
22d3c38df2 + client: handle per-client settings 2019-05-28 18:44:27 +03:00
Simon Zolin
22c7efd2d1 - openapi.yaml: fix HTTP methods 2019-05-28 18:44:27 +03:00
Simon Zolin
eb159e6997 + openapi.yaml: add /clients handlers 2019-05-28 18:44:27 +03:00
Simon Zolin
8bf76c331d + dnsfilter: use callback function for applying per-client settings 2019-05-28 18:44:27 +03:00
Simon Zolin
4bb7b654ab + dhcp: FindIPbyMAC() 2019-05-28 18:44:27 +03:00
Simon Zolin
3f89335ed2 + dns: use per-client filtering settings 2019-05-28 18:44:27 +03:00
Simon Zolin
8f7aff93d7 + clients: config: save/restore clients info array 2019-05-28 18:44:27 +03:00
Simon Zolin
5fb7e44e79 + clients API
* /clients handler: new format
+ /clients/add handler
+ /clients/delete handler
+ /clients/update handler
2019-05-28 18:44:27 +03:00
Simon Zolin
6a7b1aba8b + doc: clients 2019-05-28 18:44:27 +03:00
Simon Zolin
218f51092c Merge: + app: disable new version check and auto-update by command line switch
Close #778

* commit '9f75146eaba9d9a3ce085c884d7f18a2c628dc50':
  * client: check for empty versions response
  * docker: use --no-check-update
  * openapi: update /version.json description
  + app: disable new version check and auto-update by command line switch
2019-05-28 18:18:40 +03:00
Ildar Kamalov
9f75146eab * client: check for empty versions response 2019-05-28 15:22:48 +03:00
Simon Zolin
6ab8aa4da1 * docker: use --no-check-update 2019-05-28 11:42:50 +03:00
Simon Zolin
386886cec2 Merge: * control: 🚑 Corrects typo in parental control API error message
Close #781

* commit '517ebc0251d6bfe43b7b4d31c7c4e7e91ea928fa':
  🚑 Corrects typo in parental control API error message
2019-05-28 11:42:12 +03:00
Simon Zolin
5b29cae133 * openapi: update /version.json description 2019-05-28 11:41:36 +03:00
Simon Zolin
4df8868787 Merge: * dnsfilter: parental/safebrowsing: add setting to switch between HTTP and HTTPS #646
* commit 'f23507a5546229d8ce8f69d56667cd5212f026d3':
  * dnsfilter: parental/safebrowsing: add setting to switch between HTTP and HTTPS
2019-05-28 11:31:51 +03:00
Franck Nijhof
517ebc0251 🚑 Corrects typo in parental control API error message 2019-05-27 22:51:51 +02:00
Simon Zolin
f25639f1fc + app: disable new version check and auto-update by command line switch 2019-05-27 18:48:33 +03:00
Simon Zolin
f23507a554 * dnsfilter: parental/safebrowsing: add setting to switch between HTTP and HTTPS 2019-05-27 18:11:05 +03:00
Simon Zolin
b9df476c5d Merge: dnsforward: support IPv6
Close #735

* commit 'e2579c72bdcafed41d5be1250fb38aeda0a8184e':
  * dnsfilter: fix tests
  + dnsforward: support IPv6 (AAAA response)
  * dnsfilter: return the correct IP address (host rules)
2019-05-27 12:35:31 +03:00
Simon Zolin
e2579c72bd * dnsfilter: fix tests 2019-05-24 18:08:08 +03:00
Simon Zolin
ac8f703407 + dnsforward: support IPv6 (AAAA response)
If question type is AAAA:
 Before this patch we responded with NXDOMAIN.
 Now we send an empty response if host rule is IPv4;
 or we send an AAAA answer if host rule is IPv6.

+ block ipv6 if rule is "0.0.0.0 blockdomain"
2019-05-24 18:08:08 +03:00
Simon Zolin
9ad4bba9ab * dnsfilter: return the correct IP address (host rules) 2019-05-24 18:08:08 +03:00
Simon Zolin
452c930dd0 Merge: * dnsfilter: use 'https' for safe-browsing and parental control
Close #646

* commit '00e1b6ca089705e7fdb4764133058e04111e36df':
  * dnsfilter: use 'https' for safe-browsing and parental control
2019-05-24 18:03:00 +03:00
Simon Zolin
fdd0f594fb Merge: - control: allow requests to "/favicon.ico" while we are in install mode
Close #766

* commit 'dece393d6acfa21f588505e494eb1225adc8376a':
  - control: allow requests to "/favicon.ico" while we are in install mode
2019-05-24 18:02:07 +03:00
Simon Zolin
00e1b6ca08 * dnsfilter: use 'https' for safe-browsing and parental control 2019-05-23 17:26:50 +03:00
Simon Zolin
dece393d6a - control: allow requests to "/favicon.ico" while we are in install mode 2019-05-23 16:28:20 +03:00
Simon Zolin
aa2d942783 Merge: Update by command from UI
Close #428

* commit '70e329956776cc381fdb28805375d5b2f0e22dbf':
  * openapi: update
  * client: add link to the update error
  * client: add update timeout
  * client: add error message if update failed
  + client: handle update
  * go linter
  * control: /version.json: use new JSON format
  + set config.runningAsService
  * app: --help: more pretty help info
  + app: add --check-config command-line argument
  * app: optimize config file reading
  + /control/update handler
  * control: don't use custom resolver for tests
  + doc: Update algorithm
  - control: fix race in /control/version.json handler
2019-05-20 13:38:23 +03:00
Simon Zolin
e3ee7a0c3e Merge: dnsfilter: use urlfilter package #714
* commit '096a95998749b673bc9be638bc9c8f6f0d13be41':
  * dnsforward: use new dnsfilter interface
  * dnsfilter: adapt tests to new interface
  * dnsfilter: use urlfilter package
  * dnsfilter: remove code for filtering rules
  * dns: rename dnsfilter.Filter.Rule -> dnsfilter.Filter.Data
  * dnsforward: use separate ServerConfig object
  * use urlfilter
2019-05-20 11:00:45 +03:00
Simon Zolin
70e3299567 * openapi: update 2019-05-20 10:57:07 +03:00
Ildar Kamalov
967517316f * client: add link to the update error 2019-05-17 18:33:34 +03:00
Ildar Kamalov
24f582d36d * client: add update timeout 2019-05-17 18:33:34 +03:00
Ildar Kamalov
9cffe865ec * client: add error message if update failed 2019-05-17 18:33:34 +03:00
Ildar Kamalov
cb3f7f2834 + client: handle update 2019-05-17 18:33:34 +03:00
Simon Zolin
096a959987 * dnsforward: use new dnsfilter interface 2019-05-17 18:22:57 +03:00
Simon Zolin
5ec747b30b * dnsfilter: adapt tests to new interface 2019-05-17 18:22:57 +03:00
Simon Zolin
829415da5b * dnsfilter: use urlfilter package
+ new config setting 'filtering_temp_filename'

* remove AddRules(), modify New()
2019-05-17 18:22:57 +03:00
Simon Zolin
3396d68019 * dnsfilter: remove code for filtering rules 2019-05-17 18:22:57 +03:00
Simon Zolin
bd68bf2e25 * dns: rename dnsfilter.Filter.Rule -> dnsfilter.Filter.Data 2019-05-17 18:22:57 +03:00
Simon Zolin
9644f79a03 * dnsforward: use separate ServerConfig object 2019-05-17 18:22:57 +03:00
Simon Zolin
36e273dfd5 * use urlfilter 2019-05-17 18:22:57 +03:00
Simon Zolin
068072bc5a * go linter 2019-05-17 15:39:56 +03:00
Simon Zolin
b72ca4d127 * control: /version.json: use new JSON format 2019-05-17 15:37:38 +03:00
Simon Zolin
28440fc3ac + set config.runningAsService 2019-05-17 15:37:38 +03:00
Simon Zolin
6d14ec18ac * app: --help: more pretty help info 2019-05-17 15:37:38 +03:00
Simon Zolin
5fd35254a8 + app: add --check-config command-line argument 2019-05-17 15:37:38 +03:00
Simon Zolin
3ee8051e97 * app: optimize config file reading
* read config file just once (even when upgrading)
* don't call os.Stat()
2019-05-17 15:37:38 +03:00
Simon Zolin
2dd6ea5161 + /control/update handler 2019-05-17 15:37:38 +03:00
Simon Zolin
788e91a51e * control: don't use custom resolver for tests 2019-05-17 15:34:55 +03:00
Simon Zolin
d4fcef8d04 + doc: Update algorithm 2019-05-17 15:34:55 +03:00
Simon Zolin
392c7b6ee1 - control: fix race in /control/version.json handler 2019-05-17 10:20:41 +03:00
Simon Zolin
7bb40bca0f Merge: dns query log: robust file flushing mechanism #708
* commit 'd5f6dd1a46446ebb440811691a6ee8ce2443320d':
  - dns query log: robust file flushing mechanism
  * improve logging
2019-05-15 14:08:01 +03:00
Simon Zolin
f20cb65189 Merge: + dnsfilter: cache IP addresses of safebrowsing and parental control servers
Close #745

* commit 'd918e5b418de232d95ba1e3d642dca00664f0304':
  use maxDialCacheSize constant
  rename functions and container
  + dnsfilter: cache IP addresses of safebrowsing and parental control servers
2019-05-15 14:01:01 +03:00
Simon Zolin
d5f6dd1a46 - dns query log: robust file flushing mechanism
Before this patch we could exit the process without waiting for
 file writing task to complete.
As a result a file could become corrupted or a large chunk of data
 could be missing.

Now the main thread either waits until file writing task completes
 or it writes log buffer to file itself.
2019-05-15 13:12:03 +03:00
Simon Zolin
0f28a989e9 * improve logging 2019-05-15 13:12:03 +03:00
Simon Zolin
d918e5b418 use maxDialCacheSize constant 2019-05-15 12:03:20 +03:00
Simon Zolin
3a0f608402 Merge: dnsforward, config: add unspecified IP blocking option
Close #742, #743

* commit 'cd2dd00da300c24a88a51082ee9622a332a5b72b':
  * dnsforward_test: add test for null filter
  * dnsforward, config: add unspecified IP blocking option
2019-05-15 11:58:40 +03:00
Alexander Turcic
cd2dd00da3 * dnsforward_test: add test for null filter 2019-05-14 16:53:09 +03:00
Alexander Turcic
07ffcbec3d * dnsforward, config: add unspecified IP blocking option
* dnsforward: prioritize host files over null filter

* dnsforward, config: adjust setting variable to blocking_mode

* dnsforward: use net.IPv4zero for null IP
2019-05-14 16:53:06 +03:00
Simon Zolin
b3461d37ca rename functions and container 2019-05-13 14:47:55 +03:00
Simon Zolin
2178546e7b Merge: docker: Run as non-root user
Close #720

* commit '68f9ec70fb0f8ff2a73bf382bc15257c367b7967':
  Optimize Docker image layers; comment out runtime user; add sample docker-compose.yml
  Run as non-root user
2019-05-13 14:42:50 +03:00
Simon Zolin
24ae61de3e + dnsfilter: cache IP addresses of safebrowsing and parental control servers 2019-05-13 14:16:07 +03:00
Simon Zolin
68f9ec70fb Merge branch 'docker-improvements' of git://github.com/javabean/AdGuardHome into javabean-docker-improvements 2019-05-13 11:12:21 +03:00
Cédrik LIME
17aa46c4d2 Optimize Docker image layers; comment out runtime user; add sample docker-compose.yml 2019-05-08 21:17:14 +02:00
Ildar Kamalov
a45f0c519e Merge pull request #209 in DNS/adguard-dns from feature/734 to master
* commit '6ac9509d64e82e48bc22ccd23ac3a25776d05565':
  + client: Add a link to the list of known DNS providers to Upstream DNS settings
2019-05-06 09:35:51 +03:00
Ildar Kamalov
2cb2b3585f Merge pull request #208 in DNS/adguard-dns from fix/729 to master
* commit 'd24f208f98c08155282eb3061fd28e2e149e296b':
  - client: fixed values for settings validation
2019-05-06 09:35:42 +03:00
Ildar Kamalov
d24f208f98 - client: fixed values for settings validation
Closes #729
2019-04-28 11:43:15 +03:00
Ildar Kamalov
6ac9509d64 + client: Add a link to the list of known DNS providers to Upstream DNS settings
Closes #734
2019-04-28 11:18:56 +03:00
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
Cédrik LIME
58868b75af Run as non-root user 2019-04-17 20:48:56 +02: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
126 changed files with 8985 additions and 5479 deletions

680
AGHTechDoc.md Normal file
View File

@@ -0,0 +1,680 @@
# 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
* Updating
* Get version command
* Update command
* Device Names and Per-client Settings
* Per-client settings
* Get list of clients
* Add client
* Update client
* Delete client
* Enable DHCP server
* "Show DHCP status" command
* "Check DHCP" command
* "Enable DHCP" command
* Static IP check/set
* Add a static lease
* DNS access settings
* List access settings
* Set access settings
## 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
## Updating
Algorithm of an update by command:
* UI requests the latest version information from Server
* Server requests information from Internet; stores the data in cache for several hours; sends data to UI
* If UI sees that a new version is available, it shows notification message and "Update Now" button
* When user clicks on "Update Now" button, UI sends Update command to Server
* UI shows "Please wait, AGH is being updated..." message
* Server performs an update:
* Use working directory from `--work-dir` if necessary
* Download new package for the current OS and CPU
* Unpack the package to a temporary directory `update-vXXX`
* Copy the current configuration file to the directory we unpacked new AGH to
* Check configuration compatibility by executing `./AGH --check-config`. If this command fails, we won't be able to update.
* Create `backup-vXXX` directory and copy the current configuration file there
* Copy supporting files (README, LICENSE, etc.) to backup directory
* Copy supporting files from the update directory to the current directory
* Move the current binary file to backup directory
* Note: if power fails here, AGH won't be able to start at system boot. Administrator has to fix it manually
* Move new binary file to the current directory
* Send response to UI
* Stop all tasks, including DNS server, DHCP server, HTTP server
* If AGH is running as a service, use service control functionality to restart
* If AGH is not running as a service, use the current process arguments to start a new process
* Exit process
* UI resends Get Status command until Server responds to it with the new version. This means that Server is successfully restarted after update.
* UI reloads itself
### Get version command
On receiving this request server downloads version.json data from github and stores it in cache for several hours.
Example of version.json data:
{
"version": "v0.95-hotfix",
"announcement": "AdGuard Home v0.95-hotfix is now available!",
"announcement_url": "",
"download_windows_amd64": "",
"download_windows_386": "",
"download_darwin_amd64": "",
"download_linux_amd64": "",
"download_linux_386": "",
"download_linux_arm": "",
"download_linux_arm64": "",
"download_linux_mips": "",
"download_linux_mipsle": "",
"selfupdate_min_version": "v0.0"
}
Server can only auto-update if the current version is equal or higher than `selfupdate_min_version`.
Request:
GET /control/version.json
Response:
200 OK
{
"new_version": "v0.95",
"announcement": "AdGuard Home v0.95 is now available!",
"announcement_url": "http://...",
"can_autoupdate": true
}
If `can_autoupdate` is true, then the server can automatically upgrade to a new version.
Response with empty body:
200 OK
It means that update check is disabled by user. UI should do nothing.
### Update command
Perform an update procedure to the latest available version
Request:
POST /control/update
Response:
200 OK
Error response:
500
UI shows error message "Auto-update has failed"
## 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
### "Show DHCP status" command
Request:
GET /control/dhcp/status
Response:
200 OK
{
"config":{
"enabled":false,
"interface_name":"...",
"gateway_ip":"...",
"subnet_mask":"...",
"range_start":"...",
"range_end":"...",
"lease_duration":60,
"icmp_timeout_msec":0
},
"leases":[
{"ip":"...","mac":"...","hostname":"...","expires":"..."}
...
],
"static_leases":[
{"ip":"...","mac":"...","hostname":"..."}
...
]
}
### "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
### Add a static lease
Request:
POST /control/dhcp/add_static_lease
{
"mac":"...",
"ip":"...",
"hostname":"..."
}
Response:
200 OK
### Remove a static lease
Request:
POST /control/dhcp/remove_static_lease
{
"mac":"...",
"ip":"...",
"hostname":"..."
}
Response:
200 OK
## Device Names and Per-client Settings
When a client requests information from DNS server, he's identified by IP address.
Administrator can set a name for a client with a known IP and also override global settings for this client. The name is used to improve readability of DNS logs: client's name is shown in UI next to its IP address. The names are loaded from 3 sources:
* automatically from "/etc/hosts" file. It's a list of `IP<->Name` entries which is loaded once on AGH startup from "/etc/hosts" file.
* automatically using rDNS. It's a list of `IP<->Name` entries which is added in runtime using rDNS mechanism when a client first makes a DNS request.
* manually configured via UI. It's a list of client's names and their settings which is loaded from configuration file and stored on disk.
### Per-client settings
UI provides means to manage the list of known clients (List/Add/Update/Delete) and their settings. These settings are stored in configuration file as an array of objects.
Notes:
* `name`, `ip` and `mac` values are unique.
* `ip` & `mac` values can't be set both at the same time.
* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.
* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.
* If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings.
### Get list of clients
Request:
GET /control/clients
Response:
200 OK
{
clients: [
{
name: "client1"
ip: "..."
mac: "..."
use_global_settings: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safesearch_enabled: false
}
]
auto_clients: [
{
name: "host"
ip: "..."
source: "etc/hosts" || "rDNS"
}
]
}
### Add client
Request:
POST /control/clients/add
{
name: "client1"
ip: "..."
mac: "..."
use_global_settings: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safesearch_enabled: false
}
Response:
200 OK
Error response (Client already exists):
400
### Update client
Request:
POST /control/clients/update
{
name: "client1"
data: {
name: "client1"
ip: "..."
mac: "..."
use_global_settings: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safesearch_enabled: false
}
}
Response:
200 OK
Error response (Client not found):
400
### Delete client
Request:
POST /control/clients/delete
{
name: "client1"
}
Response:
200 OK
Error response (Client not found):
400
## DNS access settings
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
There are 3 types of access settings:
* allowed_clients: Only these clients are allowed to make DNS requests.
* disallowed_clients: These clients are not allowed to make DNS requests.
* blocked_hosts: These hosts are not allowed to be resolved by a DNS request.
### List access settings
Request:
GET /control/access/list
Response:
200 OK
{
allowed_clients: ["127.0.0.1", ...]
disallowed_clients: ["127.0.0.1", ...]
blocked_hosts: ["host.com", ...]
}
### Set access settings
Request:
POST /control/access/set
{
allowed_clients: ["127.0.0.1", ...]
disallowed_clients: ["127.0.0.1", ...]
blocked_hosts: ["host.com", ...]
}
Response:
200 OK

View File

@@ -11,14 +11,22 @@ FROM alpine:latest
LABEL maintainer="AdGuard Team <devteam@adguard.com>"
# Update CA certs
RUN apk --no-cache --update add ca-certificates && \
rm -rf /var/cache/apk/* && mkdir -p /opt/adguardhome
RUN apk --no-cache --update add ca-certificates libcap && \
rm -rf /var/cache/apk/* && \
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
chown -R nobody: /opt/adguardhome
COPY --from=build /src/AdGuardHome/AdGuardHome /opt/adguardhome/AdGuardHome
COPY --from=build --chown=nobody:nogroup /src/AdGuardHome/AdGuardHome /opt/adguardhome/AdGuardHome
EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp 3000/tcp
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
WORKDIR /opt/adguardhome/work
#USER nobody
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]

View File

@@ -2,15 +2,22 @@ FROM alpine:latest
LABEL maintainer="AdGuard Team <devteam@adguard.com>"
# Update CA certs
RUN apk --no-cache --update add ca-certificates && \
rm -rf /var/cache/apk/* && mkdir -p /opt/adguardhome
RUN apk --no-cache --update add ca-certificates libcap && \
rm -rf /var/cache/apk/* && \
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
chown -R nobody: /opt/adguardhome
COPY --chown=nobody:nogroup ./AdGuardHome /opt/adguardhome/AdGuardHome
COPY ./AdGuardHome /opt/adguardhome/AdGuardHome
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp 3000/tcp
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
WORKDIR /opt/adguardhome/work
#USER nobody
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]

View File

@@ -4,6 +4,7 @@ NATIVE_GOARCH = $(shell unset GOARCH; go env GOARCH)
GOPATH := $(shell go env GOPATH)
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
STATIC = build/static/index.html
CHANNEL ?= release
TARGET=AdGuardHome
@@ -22,7 +23,7 @@ $(STATIC): $(JSFILES) client/node_modules
$(TARGET): $(STATIC) *.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
PATH=$(GOPATH)/bin:$(PATH) packr -z
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION) -X main.updateChannel=$(CHANNEL)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
PATH=$(GOPATH)/bin:$(PATH) packr clean
clean:

107
README.md
View File

@@ -44,9 +44,15 @@ AdGuard Home is a network-wide software for blocking ads & tracking. After you s
It operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
* [Getting Started](#getting-started)
* [Comparing AdGuard Home to other solutions](#comparison)
* [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
* [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
* [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
* [How to build from source](#how-to-build)
* [Contributing](#contributing)
* [Reporting issues](#reporting-issues)
* [Test unstable versions](#test-unstable-versions)
* [Reporting issues](#reporting-issues)
* [Help with translations](#translate)
* [Acknowledgments](#acknowledgments)
<a id="getting-started"></a>
@@ -63,6 +69,52 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
* [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
* [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
<a id="comparison"></a>
## Comparing AdGuard Home to other solutions
<a id="comparison-adguard-dns"></a>
### How is this different from public AdGuard DNS servers?
Running your own AdGuard Home server allows you to do much more than using a public DNS server. It's a completely different level. See for yourself:
* Choose what exactly will the server block or not block.
* Monitor your network activity.
* Add your own custom filtering rules.
* **Most importantly, this is your own server, and you are the only one who's in control.**
<a id="comparison-pi-hole"></a>
### How does AdGuard Home compare to Pi-Hole
At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads and trackers using "DNS sinkholing" method, and both allow customizing what's blocked.
> We're not going to stop here. DNS sinkholing is not a bad starting point, but this is just the beginning.
AdGuard Home provides a lot of features out-of-the-box with no need to install and configure additional software. We want it to be simple to the point when even casual users can set it up with minimal effort.
> Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Feature | AdGuard&nbsp;Home | Pi-Hole |
|-------------------------------------------------------------------------|--------------|--------------------------------------------------------|
| Blocking ads and trackers | ✅ | ✅ |
| Customizing blocklists | ✅ | ✅ |
| Built-in DHCP server | ✅ | ✅ |
| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttp |
| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) |
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
| Blocking phishing and malware domains | ✅ | ❌ |
| Parental control (blocking adult domains) | ✅ | ❌ |
| Force Safe search on search engines | ✅ | ❌ |
| Per-client (device) configuration | ✅ | ❌ |
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
<a id="comparison-adblock"></a>
### How does AdGuard Home compare to traditional ad blockers
It depends.
"DNS sinkholing" is capable of blocking a big percentage of ads, but it lacks flexibility and power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities). However, this level of protection is enough for some users.
<a id="how-to-build"></a>
## How to build from source
@@ -89,12 +141,47 @@ cd AdGuardHome
make
```
#### (For devs) Upload translations
```
node upload.js
```
#### (For devs) Download translations
```
node download.js
```
<a id="contributing"></a>
## Contributing
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
### How to update translations
<a id="test-unstable-versions"></a>
### Test unstable versions
There are two options how you can install an unstable version.
You can either install a beta version of AdGuard Home which we update periodically,
or you can use the Docker image from the `edge` tag, which is synced with the repo master branch.
* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
* Beta builds
* [Rapsberry Pi (32-bit ARM)](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm.tar.gz)
* [MacOS](https://static.adguard.com/adguardhome/beta/AdGuardHome_MacOS.zip)
* [Windows 64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_Windows_amd64.zip)
* [Windows 32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_Windows_386.zip)
* [Linux 64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz)
* [Linux 32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
* [64-bit ARM](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz)
* [MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips.tar.gz)
* [MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle.tar.gz)
<a id="reporting-issues"></a>
### Report issues
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
<a id="translate"></a>
### Help with translations
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
@@ -118,21 +205,6 @@ Example of `oneskyapp.json`
}
```
#### Upload translations
```
node upload.js
```
#### Download translations
```
node download.js
```
<a id="reporting-issues"></a>
## Reporting issues
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
<a id="acknowledgments"></a>
## Acknowledgments
@@ -145,6 +217,7 @@ This software wouldn't have been possible without:
* [go-yaml](https://github.com/go-yaml/yaml)
* [service](https://godoc.org/github.com/kardianos/service)
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
* [urlfilter](https://github.com/AdguardTeam/urlfilter)
* [Node.js](https://nodejs.org/) and it's libraries:
* [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler)

287
app.go
View File

@@ -1,30 +1,47 @@
package main
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"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"
)
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
// updateChannel can be set via ldflags
var updateChannel = "release"
var versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json"
const versionCheckPeriod = time.Hour * 8
var httpServer *http.Server
var httpsServer struct {
server *http.Server
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
sync.Mutex // protects config.TLS
shutdown bool // if TRUE, don't restart the server
}
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
@@ -42,14 +59,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)
}
@@ -72,24 +81,51 @@ func run(args options) {
enableTLS13()
// print the first message after logger is configured
log.Printf("AdGuard Home, version %s\n", VersionString)
log.Printf("AdGuard Home, version %s, channel %s\n", VersionString, updateChannel)
log.Debug("Current working directory is %s", config.ourWorkingDir)
if args.runningAsService {
log.Info("AdGuard Home is running as a service")
}
config.runningAsService = args.runningAsService
config.disableUpdate = args.disableUpdate
config.firstRun = detectFirstRun()
// Do the upgrade if necessary
err := upgradeConfig()
if err != nil {
log.Fatal(err)
if config.firstRun {
requireAdminRights()
}
// parse from config file
err = parseConfig()
if err != nil {
log.Fatal(err)
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-signalChannel
cleanup()
cleanupAlways()
os.Exit(0)
}()
clientsInit()
if !config.firstRun {
// Do the upgrade if necessary
err := upgradeConfig()
if err != nil {
log.Fatal(err)
}
err = parseConfig()
if err != nil {
os.Exit(1)
}
if args.checkConfig {
log.Info("Configuration file is OK")
os.Exit(0)
}
}
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
config.RlimitNoFile != 0 {
setRlimit(config.RlimitNoFile)
}
// override bind host/port from the console
@@ -102,10 +138,12 @@ func run(args options) {
loadFilters()
// Save the updated config
err = config.write()
if err != nil {
log.Fatal(err)
if !config.firstRun {
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}
// Init the DNS server instance before registering HTTP handlers
@@ -113,7 +151,7 @@ func run(args options) {
initDNSServer(dnsBaseDir)
if !config.firstRun {
err = startDNSServer()
err := startDNSServer()
if err != nil {
log.Fatal(err)
}
@@ -124,6 +162,10 @@ 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)
@@ -133,8 +175,9 @@ func run(args options) {
// 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
@@ -147,56 +190,10 @@ 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.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
if !data.ValidPair {
log.Fatal(data.WarningValidation)
os.Exit(1)
}
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 {
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 {
for !httpsServer.shutdown {
printHTTPAddresses("http")
// we need to have new instance, because after Shutdown() the Server is not usable
@@ -206,11 +203,108 @@ 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
}
// wait indefinitely for other go-routines to complete their job
select {}
}
func httpServerLoop() {
for !httpsServer.shutdown {
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
@@ -298,6 +392,23 @@ func cleanup() {
}
}
// Stop HTTP server, possibly waiting for all active connections to be closed
func stopHTTPServer() {
httpsServer.shutdown = true
if httpsServer.server != nil {
httpsServer.server.Shutdown(context.TODO())
}
httpServer.Shutdown(context.TODO())
}
// This function is called before application exits
func cleanupAlways() {
if len(pidFileName) != 0 {
os.Remove(pidFileName)
}
log.Info("Stopped")
}
// command-line arguments
type options struct {
verbose bool // is verbose logging enabled
@@ -306,6 +417,9 @@ 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
checkConfig bool // Check configuration and exit
disableUpdate bool // If set, don't check for updates
// service control action (see service.ControlAction array + "status" command)
serviceControlAction string
@@ -326,24 +440,27 @@ func loadOptions() options {
callbackWithValue func(value string)
callbackNoValue func()
}{
{"config", "c", "path to the config file", func(value string) { o.configFilename = value }, nil},
{"work-dir", "w", "path to the working directory", func(value string) { o.workDir = value }, nil},
{"host", "h", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
{"port", "p", "port to serve HTTP pages on", func(value string) {
{"config", "c", "Path to the config file", func(value string) { o.configFilename = value }, nil},
{"work-dir", "w", "Path to the working directory", func(value string) { o.workDir = value }, nil},
{"host", "h", "Host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
{"port", "p", "Port to serve HTTP pages on", func(value string) {
v, err := strconv.Atoi(value)
if err != nil {
panic("Got port that is not a number")
}
o.bindPort = v
}, nil},
{"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) {
{"service", "s", "Service control action: status, install, uninstall, start, stop, restart", func(value string) {
o.serviceControlAction = value
}, nil},
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
{"logfile", "l", "Path to log file. If empty: write to stdout; if 'syslog': write to system log", func(value string) {
o.logFile = value
}, nil},
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
{"help", "", "print this help", nil, func() {
{"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil},
{"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }},
{"no-check-update", "", "Don't check for updates", nil, func() { o.disableUpdate = true }},
{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
{"help", "", "Print this help", nil, func() {
printHelp()
os.Exit(64)
}},
@@ -353,10 +470,14 @@ func loadOptions() options {
fmt.Printf("%s [options]\n\n", os.Args[0])
fmt.Printf("Options:\n")
for _, opt := range opts {
val := ""
if opt.callbackWithValue != nil {
val = " VALUE"
}
if opt.shortName != "" {
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName+val, opt.description)
} else {
fmt.Printf(" %-34s %s\n", "--"+opt.longName, opt.description)
fmt.Printf(" %-34s %s\n", "--"+opt.longName+val, opt.description)
}
}
}

3019
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

2
client/package.json vendored
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
client/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 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="favicon.ico">
<meta name="google" content="notranslate">
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
<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="favicon.ico">
<meta name="google" content="notranslate">
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
<title>Setup AdGuard Home</title>
</head>
<body>

View File

@@ -1,4 +1,5 @@
{
"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.",
@@ -11,13 +12,15 @@
"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_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "No DHCP leases found",
"dhcp_config_saved": "Saved DHCP server config",
"form_error_required": "Required field",
"form_error_ip_format": "Invalid IPv4 format",
"form_error_mac_format": "Invalid MAC format",
"form_error_positive": "Must be greater than 0",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
@@ -31,7 +34,18 @@
"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.",
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
"dhcp_new_static_lease": "New static lease",
"dhcp_static_leases_not_found": "No DHCP static leases found",
"dhcp_add_static_lease": "Add static lease",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Enter hostname",
"error_details": "Error details",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
@@ -45,6 +59,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",
@@ -77,10 +92,14 @@
"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",
"dns_settings": "DNS settings",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"client_settings": "Client 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.",
"test_upstream_btn": "Test upstreams",
@@ -99,6 +118,7 @@
"rules_count_table_header": "Rules count",
"last_time_updated_table_header": "Last time updated",
"actions_table_header": "Actions",
"edit_table_action": "Edit",
"delete_table_action": "Delete",
"filters_and_hosts": "Filters and hosts blocklists",
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
@@ -121,11 +141,10 @@
"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 <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_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)",
"example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)</0>",
"all_filters_up_to_date_toast": "All filters are already up-to-date",
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
"dns_test_ok_toast": "Specified DNS servers are working correctly",
@@ -252,5 +271,46 @@
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
"setup_guide": "Setup guide",
"dns_addresses": "DNS addresses"
"dns_addresses": "DNS addresses",
"down": "Down",
"fix": "Fix",
"dns_providers": "Here is a <0>list of known DNS providers<\/0> to choose from.",
"update_now": "Update now",
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
"processing_update": "Please wait, AdGuard Home is being updated",
"clients_title": "Clients",
"clients_desc": "Configure devices connected to AdGuard Home",
"settings_global": "Global",
"settings_custom": "Custom",
"table_client": "Client",
"table_name": "Name",
"save_btn": "Save",
"client_add": "Add Client",
"client_new": "New Client",
"client_edit": "Edit Client",
"client_identifier": "Identifier",
"ip_address": "IP address",
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
"form_enter_ip": "Enter IP",
"form_enter_mac": "Enter MAC",
"form_client_name": "Enter client name",
"client_global_settings": "Use global settings",
"client_deleted": "Client \"{{key}}\" successfully deleted",
"client_added": "Client \"{{key}}\" successfully added",
"client_updated": "Client \"{{key}}\" successfully updated",
"table_statistics": "Requests count (last 24 hours)",
"clients_not_found": "No clients found",
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
"filter_confirm_delete": "Are you sure you want to delete filter?",
"auto_clients_title": "Clients (runtime)",
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
"access_disallowed_title": "Disallowed clients",
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_blocked_title": "Blocked domains",
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
"access_settings_saved": "Access settings successfully saved"
}

View File

@@ -1,132 +1,158 @@
{
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
"save_config": "Guardar config",
"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 usted especifique como DNS de subida.",
"url_added_successfully": "URL a\u00f1adida correctamente",
"check_dhcp_servers": "Comprobar si hay servidores DHCP",
"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_leases_not_found": "No se encontraron concesi\u00f3nes DHCP",
"dhcp_disable": "Deshabilitar servidor DHCP",
"dhcp_not_found": "Es seguro habilitar el servidor DHCP incorporado. No se ha encontrado ning\u00fan servidor DHCP activo en la red, sin embargo le recomendamos que lo vuelva a comprobar manualmente, ya que nuestra prueba autom\u00e1tica no ofrece actualmente una garant\u00eda del 100%.",
"dhcp_found": "Un servidor DHCP activo se encuentra en la red. No es seguro habilitar el servidor DHCP incorporado.",
"dhcp_leases": "Asignaciones DHCP",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "No se han encontrado asignaciones 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_mac_format": "Formato MAC 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_title": "Tiempo de asignaci\u00f3n DHCP (en segundos)",
"dhcp_form_lease_input": "Duraci\u00f3n de asignaci\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 de todos modos desea habilitar el servidor DHCP, aseg\u00farese de que no hay otro servidor DHCP activo en su red. \u00a1De lo contrario, puede dejar sin Internet a los dispositivos conectados!",
"dhcp_error": "No pudimos determinar si hay otro servidor DHCP en la red.",
"dhcp_static_ip_error": "Para poder utilizar el servidor DHCP se debe establecer una direcci\u00f3n IP est\u00e1tica. No hemos podido determinar si esta interfaz de red est\u00e1 configurada utilizando una direcci\u00f3n IP est\u00e1tica. Por favor establezca una direcci\u00f3n IP est\u00e1tica manualmente.",
"dhcp_dynamic_ip_found": "Su sistema utiliza la configuraci\u00f3n de direcci\u00f3n IP din\u00e1mica para la interfaz <0>{{interfaceName}}<\/0>. Para poder utilizar el servidor DHCP se debe establecer una direcci\u00f3n IP est\u00e1tica. Su direcci\u00f3n IP actual es <0>{{ipAddress}}<\/0>. Si presiona el bot\u00f3n Habilitar servidor DHCP, estableceremos autom\u00e1ticamente esta direcci\u00f3n IP como est\u00e1tica.",
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
"dhcp_new_static_lease": "New static lease",
"dhcp_static_leases_not_found": "No DHCP static leases found",
"dhcp_add_static_lease": "Add static lease",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Enter hostname",
"error_details": "Detalles del error",
"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",
"privacy_policy": "Pol\u00edtica de privacidad",
"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 web 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",
"no_domains_found": "No se han encontrado dominios",
"requests_count": "N\u00famero de peticiones",
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
"top_clients": "Clientes m\u00e1s populares",
"no_clients_found": "No hay clientes",
"top_clients": "Clientes m\u00e1s frecuentes",
"no_clients_found": "No se han encontrado 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 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 seguridad de navegaci\u00f3n de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "N\u00famero de sitios web 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 ",
"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.",
"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 la API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: solo se env\u00eda al servidor un prefijo corto del nombre de dominio con hash 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 del 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, DuckDuckGo 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",
"dns_settings": "DNS settings",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"client_settings": "Client settings",
"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",
"edit_table_action": "Editar",
"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",
"filters_and_hosts_hint": "AdGuard Home entiende las reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos hosts.",
"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",
"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.",
"enter_name_hint": "Ingrese el nombre",
"enter_url_hint": "Ingrese la URL",
"check_updates_btn": "Buscar actualizaciones",
"new_filter_btn": "Nueva suscripci\u00f3n a filtro",
"enter_valid_filter_url": "Ingrese una URL v\u00e1lida para suscribirse a un filtro o archivo hosts.",
"custom_filter_rules": "Reglas de filtrado personalizado",
"custom_filter_rules_hint": "Ingrese una regla por l\u00ednea. Puede utilizar reglas de bloqueo o la sintaxis de los archivos 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 <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
"example_upstream_doh": "encriptado <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
"example_upstream_sdns": "puedes usar <0>DNS Stamps<\/0> para <1>DNSCrypt<\/1> o <2>DNS-over-HTTPS<\/2> 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",
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito 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 se puede utilizar, 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 +160,157 @@
"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 para bloqueo de anuncios y rastreadores a nivel de 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 de 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 en 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 el 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 de 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 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 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 a 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",
"down": "Abajo",
"fix": "Corregir",
"dns_providers": "Aqu\u00ed hay una <0>lista de proveedores DNS<\/0> conocidos para elegir.",
"update_now": "Actualizar ahora",
"update_failed": "Error en la actualizaci\u00f3n autom\u00e1tica. Por favor <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>siga los pasos<\/a> para actualizar manualmente.",
"processing_update": "Por favor espere, AdGuard Home se est\u00e1 actualizando",
"clients_title": "Clientes",
"clients_desc": "Configurar dispositivos conectados con AdGuard Home",
"settings_global": "Global",
"settings_custom": "Personalizado",
"table_client": "Cliente",
"table_name": "Nombre",
"save_btn": "Guardar",
"client_add": "A\u00f1adir cliente",
"client_new": "Cliente nuevo",
"client_edit": "Editar cliente",
"client_identifier": "Identificador",
"ip_address": "Direcci\u00f3n IP",
"client_identifier_desc": "Los clientes pueden ser identificados por la direcci\u00f3n IP o MAC. Tenga en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home tambi\u00e9n es un <0>servidor DHCP<\/0>.",
"form_enter_ip": "Ingresar IP",
"form_enter_mac": "Ingresar MAC",
"form_client_name": "Ingrese el nombre del cliente",
"client_global_settings": "Usar configuraci\u00f3n global",
"client_deleted": "Cliente \"{{key}}\" eliminado correctamente",
"client_added": "Cliente \"{{key}}\" a\u00f1adido correctamente",
"client_updated": "Cliente \"{{key}}\" actualizado correctamente",
"table_statistics": "N\u00famero de peticiones (\u00faltimas 24 horas)",
"clients_not_found": "No se han encontrado clientes",
"client_confirm_delete": "\u00bfEst\u00e1 seguro de que desea eliminar el cliente \"{{key}}\"?",
"filter_confirm_delete": "Are you sure you want to delete filter?",
"auto_clients_title": "Clientes (activos)",
"auto_clients_desc": "Datos de los clientes que utilizan AdGuard Home pero que no est\u00e1n almacenados en la configuraci\u00f3n",
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
"access_disallowed_title": "Disallowed clients",
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_blocked_title": "Blocked domains",
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
"access_settings_saved": "Access settings successfully saved"
}

View File

@@ -1,4 +1,8 @@
{
"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",
@@ -11,10 +15,12 @@
"dhcp_not_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. \u00c9 seguro ativar o servidor DHCP integrado.",
"dhcp_found": "Foram encontrados servidores DHCP ativos na rede. N\u00e3o \u00e9 seguro ativar o servidor DHCP integrado.",
"dhcp_leases": "Concess\u00f5es DHCP",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "Nenhuma concess\u00e3o DHCP encontrada",
"dhcp_config_saved": "Salvar configura\u00e7\u00f5es do servidor DHCP",
"form_error_required": "Campo obrigat\u00f3rio",
"form_error_ip_format": "formato de endere\u00e7o IPv4 inv\u00e1lido",
"form_error_mac_format": "Invalid MAC format",
"form_error_positive": "Deve ser maior que 0",
"dhcp_form_gateway_input": "IP do gateway",
"dhcp_form_subnet_input": "M\u00e1scara de sub-rede",
@@ -28,7 +34,18 @@
"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!",
"dhcp_warning": "Se voc\u00ea quiser ativar o servidor DHCP, verifique se n\u00e3o h\u00e1 outro servidor DHCP ativo na sua rede. Caso contr\u00e1rio, a internet pode parar de funcionar para outros dispositivos conectados!",
"dhcp_error": "N\u00e3o foi poss\u00edvel determinar se existe outro servidor DHCP na rede.",
"dhcp_static_ip_error": "Para usar o servidor DHCP, voc\u00ea deve definir um endere\u00e7o IP est\u00e1tico. N\u00e3o conseguimos determinar se essa interface de rede est\u00e1 configurada usando o endere\u00e7o de IP est\u00e1tico. Por favor, defina um endere\u00e7o IP est\u00e1tico manualmente.",
"dhcp_dynamic_ip_found": "Seu sistema usa a configura\u00e7\u00e3o de endere\u00e7o IP din\u00e2mico para a interface <0>{{interfaceName}}<\/0>. Para usar o servidor DHCP, voc\u00ea deve definir um endere\u00e7o de IP est\u00e1tico. Seu endere\u00e7o IP atual \u00e9 <0> {{ipAddress}} <\/ 0>. Vamos definir automaticamente este endere\u00e7o IP como est\u00e1tico se voc\u00ea pressionar o bot\u00e3o Ativar DHCP.",
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
"dhcp_new_static_lease": "New static lease",
"dhcp_static_leases_not_found": "No DHCP static leases found",
"dhcp_add_static_lease": "Add static lease",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Enter hostname",
"error_details": "Detalhes do erro",
"back": "Voltar",
"dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es",
@@ -42,6 +59,7 @@
"copyright": "Copyright",
"homepage": "P\u00e1gina inicial",
"report_an_issue": "Reportar um problema",
"privacy_policy": "Pol\u00edtica de privacidade",
"enable_protection": "Ativar prote\u00e7\u00e3o",
"enabled_protection": "Prote\u00e7\u00e3o ativada",
"disable_protection": "Desativar prote\u00e7\u00e3o",
@@ -78,8 +96,12 @@
"no_servers_specified": "Nenhum servidor especificado",
"no_settings": "N\u00e3o configurado",
"general_settings": "Configura\u00e7\u00f5es gerais",
"dns_settings": "DNS settings",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"client_settings": "Client settings",
"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",
@@ -96,6 +118,7 @@
"rules_count_table_header": "Quantidade de regras",
"last_time_updated_table_header": "\u00daltima atualiza\u00e7\u00e3o",
"actions_table_header": "A\u00e7\u00f5es",
"edit_table_action": "Edit",
"delete_table_action": "Excluir",
"filters_and_hosts": "Filtros e listas de bloqueio de hosts",
"filters_and_hosts_hint": "O AdGuard Home entende regras b\u00e1sicas de bloqueio de an\u00fancios e a sintaxe de arquivos de hosts.",
@@ -118,9 +141,9 @@
"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 <0>atrav\u00e9s do TLS<\/0>",
"example_upstream_doh": "DNS criptografado <0>atrav\u00e9s do HTTPS<\/0>",
"example_upstream_sdns": "Voc\u00ea pode usar <0>DNS Stamps<\/0> para o <1>DNSCrypt<\/1> ou usar resolvedores <2>DNS-sobre-HTTPS<\/2>",
"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",
@@ -211,26 +234,26 @@
"open_dashboard": "Abrir painel",
"install_saved": "Salvo com sucesso",
"encryption_title": "Criptografia",
"encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"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": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"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": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"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": "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_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": "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_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": "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_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",
@@ -246,5 +269,48 @@
"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."
"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",
"down": "Caiu",
"fix": "Corrigido",
"dns_providers": "Aqui est\u00e1 uma <0>lista de provedores de DNS conhecidos<\/0> para escolher.",
"update_now": "Update now",
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
"processing_update": "Please wait, AdGuard Home is being updated",
"clients_title": "Clients",
"clients_desc": "Configure devices connected to AdGuard Home",
"settings_global": "Global",
"settings_custom": "Custom",
"table_client": "Client",
"table_name": "Name",
"save_btn": "Save",
"client_add": "Add Client",
"client_new": "New Client",
"client_edit": "Edit Client",
"client_identifier": "Identifier",
"ip_address": "IP address",
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
"form_enter_ip": "Enter IP",
"form_enter_mac": "Enter MAC",
"form_client_name": "Enter client name",
"client_global_settings": "Use global settings",
"client_deleted": "Client \"{{key}}\" successfully deleted",
"client_added": "Client \"{{key}}\" successfully added",
"client_updated": "Client \"{{key}}\" successfully updated",
"table_statistics": "Requests count (last 24 hours)",
"clients_not_found": "No clients found",
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
"filter_confirm_delete": "Are you sure you want to delete filter?",
"auto_clients_title": "Clients (runtime)",
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
"access_disallowed_title": "Disallowed clients",
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_blocked_title": "Blocked domains",
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
"access_settings_saved": "Access settings successfully saved"
}

View File

@@ -1,24 +1,27 @@
{
"example_upstream_reserved": "\u60a8\u53ef\u660e\u78ba\u6307\u5b9a<0>\u7528\u65bc\u7279\u5b9a\u7684\u7db2\u57df<\/0>\u4e4b DNS \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",
"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\u7684 DoH\/DoT \u89e3\u6790\u5668\u4e4b IP \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",
"enabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u555f\u7528",
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u7981\u7528",
"dhcp_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff08\u5be6\u9a57\u6027\u7684\uff01\uff09",
"dhcp_description": "\u5982\u679c\u60a8\u7684\u8def\u7531\u5668\u672a\u63d0\u4f9b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u8a2d\u5b9a\uff0c\u60a8\u53ef\u4f7f\u7528AdGuard\u81ea\u8eab\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u3002",
"dhcp_description": "\u5982\u679c\u60a8\u7684\u8def\u7531\u5668\u672a\u63d0\u4f9b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u8a2d\u5b9a\uff0c\u60a8\u53ef\u4f7f\u7528 AdGuard \u81ea\u8eab\u5167\u5efa\u7684 DHCP \u4f3a\u670d\u5668\u3002",
"dhcp_enable": "\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_disable": "\u7981\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_not_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u7121\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u67d0\u4e9b\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_not_found": "\u555f\u7528\u5167\u5efa\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684 - \u65bc\u8a72\u7db2\u8def\u4e0a\uff0c\u6211\u5011\u672a\u767c\u73fe\u4efb\u4f55\u73fe\u884c\u7684 DHCP \u4f3a\u670d\u5668\u3002\u7136\u800c\uff0c\u6211\u5011\u9f13\u52f5\u60a8\u624b\u52d5\u5730\u91cd\u65b0\u6aa2\u67e5\u5b83\uff0c\u56e0\u70ba\u6211\u5011\u7684\u81ea\u52d5\u4e4b\u6e2c\u8a66\u76ee\u524d\u4e0d\u4e88 100\uff05 \u4fdd\u8b49\u3002",
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\uff0c\u4e00\u500b\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684 DHCP \u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_config_saved": "\u5df2\u5132\u5b58\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u914d\u7f6e",
"form_error_required": "\u5fc5\u586b\u7684\u6b04\u4f4d",
"form_error_ip_format": "\u7121\u6548\u7684IPv4\u683c\u5f0f",
"form_error_positive": "\u5fc5\u9808\u5927\u65bc0",
"form_error_ip_format": "\u7121\u6548\u7684 IPv4 \u683c\u5f0f",
"form_error_mac_format": "\u7121\u6548\u7684\u5a92\u9ad4\u5b58\u53d6\u63a7\u5236\uff08MAC\uff09\u683c\u5f0f",
"form_error_positive": "\u5fc5\u9808\u5927\u65bc 0",
"dhcp_form_gateway_input": "\u9598\u9053 IP",
"dhcp_form_subnet_input": "\u5b50\u7db2\u8def\u906e\u7f69",
"dhcp_form_range_title": "IP \u4f4d\u5740\u7bc4\u570d",
@@ -31,7 +34,18 @@
"dhcp_ip_addresses": "IP \u4f4d\u5740",
"dhcp_table_hostname": "\u4e3b\u6a5f\u540d\u7a31",
"dhcp_table_expires": "\u5230\u671f",
"dhcp_warning": "\u5982\u679c\u60a8\u60f3\u8981\u555f\u7528\u5167\u5efa\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u78ba\u4fdd\u7121\u5176\u5b83\u73fe\u884c\u7684DHCP\u4f3a\u670d\u5668\u3002\u5426\u5247\uff0c\u5b83\u53ef\u80fd\u6703\u7834\u58de\u4f9b\u5df2\u9023\u7dda\u7684\u88dd\u7f6e\u4e4b\u7db2\u969b\u7db2\u8def\uff01",
"dhcp_warning": "\u5982\u679c\u60a8\u7121\u8ad6\u5982\u4f55\u60f3\u8981\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u78ba\u4fdd\u5728\u60a8\u7684\u7db2\u8def\u7121\u5176\u5b83\u73fe\u884c\u7684 DHCP \u4f3a\u670d\u5668\u3002\u5426\u5247\uff0c\u5b83\u53ef\u80fd\u6703\u7834\u58de\u4f9b\u5df2\u9023\u7dda\u7684\u88dd\u7f6e\u4e4b\u7db2\u969b\u7db2\u8def\uff01",
"dhcp_error": "\u6211\u5011\u7121\u6cd5\u78ba\u5b9a\u5728\u8a72\u7db2\u8def\u662f\u5426\u6709\u53e6\u5916\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002",
"dhcp_static_ip_error": "\u70ba\u4e86\u4f7f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u975c\u614b IP \u4f4d\u5740\u5fc5\u9808\u88ab\u8a2d\u5b9a\u3002\u6211\u5011\u672a\u80fd\u78ba\u5b9a\u8a72\u7db2\u8def\u4ecb\u9762\u662f\u5426\u88ab\u914d\u7f6e\u4f7f\u7528\u975c\u614b IP \u4f4d\u5740\u3002\u8acb\u624b\u52d5\u5730\u8a2d\u5b9a\u975c\u614b IP \u4f4d\u5740\u3002",
"dhcp_dynamic_ip_found": "\u60a8\u7684\u7cfb\u7d71\u4f7f\u7528\u52d5\u614b IP \u4f4d\u5740\u914d\u7f6e\u4f9b\u4ecb\u9762 <0>{{interfaceName}}<\/0>\u3002\u70ba\u4e86\u4f7f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u975c\u614bIP\u4f4d\u5740\u5fc5\u9808\u88ab\u8a2d\u5b9a\u3002\u60a8\u73fe\u884c\u7684 IP \u4f4d\u5740\u70ba <0>{{ipAddress}}<\/0>\u3002\u5982\u679c\u60a8\u6309\u555f\u7528 DHCP \u6309\u9215\uff0c\u6211\u5011\u5c07\u81ea\u52d5\u5730\u8a2d\u5b9a\u6b64 IP \u4f4d\u5740\u4f5c\u70ba\u975c\u614b\u3002",
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
"dhcp_new_static_lease": "New static lease",
"dhcp_static_leases_not_found": "No DHCP static leases found",
"dhcp_add_static_lease": "Add static lease",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Enter hostname",
"error_details": "\u932f\u8aa4\u7d30\u7bc0",
"back": "\u8fd4\u56de",
"dashboard": "\u5100\u8868\u677f",
"settings": "\u8a2d\u5b9a",
@@ -45,6 +59,7 @@
"copyright": "\u7248\u6b0a",
"homepage": "\u9996\u9801",
"report_an_issue": "\u5831\u544a\u554f\u984c",
"privacy_policy": "\u96b1\u79c1\u653f\u7b56",
"enable_protection": "\u555f\u7528\u9632\u8b77",
"enabled_protection": "\u5df2\u555f\u7528\u9632\u8b77",
"disable_protection": "\u7981\u7528\u9632\u8b77",
@@ -55,34 +70,38 @@
"stats_malware_phishing": "\u5df2\u5c01\u9396\u7684\u60e1\u610f\u8edf\u9ad4\/\u7db2\u8def\u91e3\u9b5a",
"stats_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9",
"stats_query_domain": "\u71b1\u9580\u5df2\u67e5\u8a62\u7684\u7db2\u57df",
"for_last_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167",
"for_last_24_hours": "\u5728\u6700\u8fd1\u7684 24 \u5c0f\u6642\u5167",
"no_domains_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7db2\u57df",
"requests_count": "\u8acb\u6c42\u7e3d\u6578",
"top_blocked_domains": "\u71b1\u9580\u5df2\u5c01\u9396\u7684\u7db2\u57df",
"top_clients": "\u71b1\u9580\u7528\u6236\u7aef",
"no_clients_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7528\u6236\u7aef",
"general_statistics": "\u4e00\u822c\u7684\u7d71\u8a08\u8cc7\u6599",
"number_of_dns_query_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684DNS\u67e5\u8a62\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours": "\u5df2\u88ab\u5ee3\u544a\u5c01\u9396\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88abAdGuard\u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_24_hours": "\u5728\u6700\u8fd1\u7684 24 \u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684 DNS \u67e5\u8a62\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours": "\u5df2\u88ab\u5ee3\u544a\u5c01\u9396\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae\u5c01\u9396\u7684 DNS \u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88ab AdGuard \u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684 DNS \u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9\u4e4b\u6578\u91cf",
"enforced_save_search": "\u5df2\u5f37\u5236\u57f7\u884c\u7684\u5b89\u5168\u641c\u5c0b",
"number_of_dns_query_to_safe_search": "\u5c0d\u65bc\u90a3\u4e9b\u5b89\u5168\u641c\u5c0b\u5df2\u88ab\u5f37\u5236\u57f7\u884c\u4e4b\u5c6c\u65bc\u641c\u5c0b\u5f15\u64ce\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_to_safe_search": "\u5c0d\u65bc\u90a3\u4e9b\u5b89\u5168\u641c\u5c0b\u5df2\u88ab\u5f37\u5236\u57f7\u884c\u4e4b\u5c6c\u65bc\u641c\u5c0b\u5f15\u64ce\u7684 DNS \u8acb\u6c42\u4e4b\u6578\u91cf",
"average_processing_time": "\u5e73\u5747\u7684\u8655\u7406\u6642\u9593",
"average_processing_time_hint": "\u65bc\u8655\u7406\u4e00\u9805DNS\u8acb\u6c42\u4e0a\u4ee5\u6beb\u79d2\uff08ms\uff09\u8a08\u4e4b\u5e73\u5747\u7684\u6642\u9593",
"average_processing_time_hint": "\u65bc\u8655\u7406\u4e00\u9805 DNS \u8acb\u6c42\u4e0a\u4ee5\u6beb\u79d2\uff08ms\uff09\u8a08\u4e4b\u5e73\u5747\u7684\u6642\u9593",
"block_domain_use_filters_and_hosts": "\u900f\u904e\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u6a94\u6848\u5c01\u9396\u7db2\u57df",
"filters_block_toggle_hint": "\u60a8\u53ef\u5728<a href='#filters'>\u904e\u6ffe\u5668<\/a>\u8a2d\u5b9a\u4e2d\u8a2d\u7f6e\u5c01\u9396\u898f\u5247\u3002",
"use_adguard_browsing_sec": "\u4f7f\u7528AdGuard\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9",
"use_adguard_browsing_sec_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u88ab\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u5217\u5165\u9ed1\u540d\u55ae\u3002\u5b83\u5c07\u4f7f\u7528\u53cb\u597d\u7684\u96b1\u79c1\u67e5\u627e\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u4ee5\u57f7\u884c\u6aa2\u67e5\uff1a\u50c5\u57df\u540dSHA256\u96dc\u6e4a\u7684\u77ed\u524d\u7db4\u88ab\u50b3\u9001\u5230\u4f3a\u670d\u5668\u3002",
"use_adguard_parental": "\u4f7f\u7528AdGuard\u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
"use_adguard_parental_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
"use_adguard_browsing_sec": "\u4f7f\u7528 AdGuard \u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9",
"use_adguard_browsing_sec_hint": "AdGuard Home \u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u88ab\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u5217\u5165\u9ed1\u540d\u55ae\u3002\u5b83\u5c07\u4f7f\u7528\u53cb\u597d\u7684\u96b1\u79c1\u67e5\u627e\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u4ee5\u57f7\u884c\u6aa2\u67e5\uff1a\u50c5\u57df\u540d SHA256 \u96dc\u6e4a\u7684\u77ed\u524d\u7db4\u88ab\u50b3\u9001\u5230\u4f3a\u670d\u5668\u3002",
"use_adguard_parental": "\u4f7f\u7528 AdGuard \u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
"use_adguard_parental_hint": "AdGuard Home \u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
"enforce_safe_search": "\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b",
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4e0b\u5217\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"enforce_save_search_hint": "AdGuard Home \u53ef\u5728\u4e0b\u5217\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u3001DuckDuckGo \u548c Yandex \u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"no_servers_specified": "\u7121\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u4f3a\u670d\u5668",
"no_settings": "\u7121\u8a2d\u5b9a",
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
"upstream_dns": "\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"upstream_dns_hint": "\u5982\u679c\u60a8\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",
"dns_settings": "DNS settings",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"client_settings": "Clients settings",
"upstream_dns": "\u4e0a\u6e38\u7684 DNS \u4f3a\u670d\u5668",
"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",
@@ -99,9 +118,10 @@
"rules_count_table_header": "\u898f\u5247\u7e3d\u6578",
"last_time_updated_table_header": "\u6700\u8fd1\u7684\u66f4\u65b0\u6642\u9593",
"actions_table_header": "\u884c\u52d5",
"edit_table_action": "\u7de8\u8f2f",
"delete_table_action": "\u522a\u9664",
"filters_and_hosts": "\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae",
"filters_and_hosts_hint": "AdGuard Home\u61c2\u5f97\u57fa\u672c\u7684\u5ee3\u544a\u5c01\u9396\u898f\u5247\u548c\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
"filters_and_hosts_hint": "AdGuard Home \u61c2\u5f97\u57fa\u672c\u7684\u5ee3\u544a\u5c01\u9396\u898f\u5247\u548c\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
"no_filters_added": "\u7121\u5df2\u52a0\u5165\u7684\u904e\u6ffe\u5668",
"add_filter_btn": "\u589e\u52a0\u904e\u6ffe\u5668",
"cancel_btn": "\u53d6\u6d88",
@@ -113,21 +133,21 @@
"custom_filter_rules": "\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
"custom_filter_rules_hint": "\u65bc\u4e00\u884c\u4e0a\u8f38\u5165\u4e00\u500b\u898f\u5247\u3002\u60a8\u53ef\u4f7f\u7528\u5ee3\u544a\u5c01\u9396\u898f\u5247\u6216\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
"examples_title": "\u7bc4\u4f8b",
"example_meaning_filter_block": "\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_filter_whitelist": "\u89e3\u9664\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_host_block": "AdGuard Home\u73fe\u5728\u5c07\u5c0dexample.org\u7db2\u57df\u8fd4\u56de127.0.0.1\u4f4d\u5740\uff08\u4f46\u975e\u5176\u5b50\u7db2\u57df\uff09\u3002",
"example_meaning_filter_block": "\u5c01\u9396\u81f3 example.org \u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_filter_whitelist": "\u89e3\u9664\u5c01\u9396\u81f3 example.org \u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_host_block": "AdGuard Home \u73fe\u5728\u5c07\u5c0d example.org \u7db2\u57df\u8fd4\u56de 127.0.0.1 \u4f4d\u5740\uff08\u4f46\u975e\u5176\u5b50\u7db2\u57df\uff09\u3002",
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
"example_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_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904e UDP\uff09",
"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 <0>DNSCrypt<\/0> \u6216 <1>DNS-over-HTTPS<\/1> \u89e3\u6790\u5668\u4e4b <2>DNS \u6233\u8a18<\/2>",
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
"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\u904e TCP\uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684",
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"dns_test_ok_toast": "\u5df2\u660e\u78ba\u6307\u5b9a\u7684DNS\u4f3a\u670d\u5668\u6b63\u5728\u6b63\u78ba\u5730\u904b\u4f5c",
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684 DNS \u4f3a\u670d\u5668",
"dns_test_ok_toast": "\u5df2\u660e\u78ba\u6307\u5b9a\u7684 DNS \u4f3a\u670d\u5668\u6b63\u5728\u6b63\u78ba\u5730\u904b\u4f5c",
"dns_test_not_ok_toast": "\u4f3a\u670d\u5668 \"{{key}}\"\uff1a\u7121\u6cd5\u88ab\u4f7f\u7528\uff0c\u8acb\u6aa2\u67e5\u60a8\u5df2\u6b63\u78ba\u5730\u586b\u5beb\u5b83",
"unblock_btn": "\u89e3\u9664\u5c01\u9396",
"block_btn": "\u5c01\u9396",
@@ -144,7 +164,7 @@
"download_log_file_btn": "\u4e0b\u8f09\u8a18\u9304\u6a94\u6848",
"refresh_btn": "\u91cd\u65b0\u6574\u7406",
"enabled_log_btn": "\u555f\u7528\u8a18\u9304",
"last_dns_queries": "\u6700\u8fd1\u76845000\u7b46DNS\u67e5\u8a62",
"last_dns_queries": "\u6700\u8fd1\u7684 5000 \u7b46 DNS \u67e5\u8a62",
"previous_btn": "\u4e0a\u4e00\u9801",
"next_btn": "\u4e0b\u4e00\u9801",
"loading_table_status": "\u6b63\u5728\u8f09\u5165...",
@@ -162,18 +182,18 @@
"rule_label": "\u898f\u5247",
"filter_label": "\u904e\u6ffe\u5668",
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}",
"install_welcome_title": "\u6b61\u8fce\u81f3AdGuard Home\uff01",
"install_welcome_desc": "AdGuard Home\u662f\u5168\u7db2\u8def\u7bc4\u570d\u5ee3\u544a\u548c\u8ffd\u8e64\u5668\u5c01\u9396\u7684DNS\u4f3a\u670d\u5668\u3002\u5b83\u7684\u76ee\u7684\u70ba\u8b93\u60a8\u63a7\u5236\u60a8\u6574\u500b\u7684\u7db2\u8def\u548c\u6240\u6709\u60a8\u7684\u88dd\u7f6e\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u7528\u6236\u7aef\u7a0b\u5f0f\u3002",
"install_welcome_title": "\u6b61\u8fce\u81f3 AdGuard Home\uff01",
"install_welcome_desc": "AdGuard Home \u662f\u5168\u7db2\u8def\u7bc4\u570d\u5ee3\u544a\u548c\u8ffd\u8e64\u5668\u5c01\u9396\u7684 DNS \u4f3a\u670d\u5668\u3002\u5b83\u7684\u76ee\u7684\u70ba\u8b93\u60a8\u63a7\u5236\u60a8\u6574\u500b\u7684\u7db2\u8def\u548c\u6240\u6709\u60a8\u7684\u88dd\u7f6e\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u7528\u6236\u7aef\u7a0b\u5f0f\u3002",
"install_settings_title": "\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762",
"install_settings_listen": "\u76e3\u807d\u4ecb\u9762",
"install_settings_port": "\u9023\u63a5\u57e0",
"install_settings_interface_link": "\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5c07\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u70ba\u53ef\u7528\u7684\uff1a",
"install_settings_interface_link": "\u60a8\u7684 AdGuard Home \u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5c07\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u70ba\u53ef\u7528\u7684\uff1a",
"form_error_port": "\u8f38\u5165\u6709\u6548\u7684\u9023\u63a5\u57e0\u503c",
"install_settings_dns": "DNS \u4f3a\u670d\u5668",
"install_settings_dns_desc": "\u60a8\u5c07\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u6216\u8def\u7531\u5668\u4ee5\u4f7f\u7528\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u4e4bDNS\u4f3a\u670d\u5668\uff1a",
"install_settings_dns_desc": "\u60a8\u5c07\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u6216\u8def\u7531\u5668\u4ee5\u4f7f\u7528\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u4e4b DNS \u4f3a\u670d\u5668\uff1a",
"install_settings_all_interfaces": "\u6240\u6709\u7684\u4ecb\u9762",
"install_auth_title": "\u9a57\u8b49",
"install_auth_desc": "\u88ab\u975e\u5e38\u5efa\u8b70\u914d\u7f6e\u5c6c\u65bc\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u4e4b\u5bc6\u78bc\u9a57\u8b49\u3002\u5373\u4f7f\u5b83\u50c5\u5728\u60a8\u7684\u5340\u57df\u7db2\u8def\u4e2d\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u8b93\u5b83\u53d7\u4fdd\u8b77\u514d\u65bc\u4e0d\u53d7\u9650\u5236\u7684\u5b58\u53d6\u70ba\u4ecd\u7136\u91cd\u8981\u7684\u3002",
"install_auth_desc": "\u88ab\u975e\u5e38\u5efa\u8b70\u914d\u7f6e\u5c6c\u65bc\u60a8\u7684 AdGuard Home \u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u4e4b\u5bc6\u78bc\u9a57\u8b49\u3002\u5373\u4f7f\u5b83\u50c5\u5728\u60a8\u7684\u5340\u57df\u7db2\u8def\u4e2d\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u8b93\u5b83\u53d7\u4fdd\u8b77\u514d\u65bc\u4e0d\u53d7\u9650\u5236\u7684\u5b58\u53d6\u70ba\u4ecd\u7136\u91cd\u8981\u7684\u3002",
"install_auth_username": "\u7528\u6236\u540d",
"install_auth_password": "\u5bc6\u78bc",
"install_auth_confirm": "\u78ba\u8a8d\u5bc6\u78bc",
@@ -181,50 +201,50 @@
"install_auth_password_enter": "\u8f38\u5165\u5bc6\u78bc",
"install_step": "\u6b65\u9a5f",
"install_devices_title": "\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e",
"install_devices_desc": "\u70ba\u4f7fAdGuard Home\u958b\u59cb\u904b\u4f5c\uff0c\u60a8\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u4ee5\u4f7f\u7528\u5b83\u3002",
"install_devices_desc": "\u70ba\u4f7f AdGuard Home \u958b\u59cb\u904b\u4f5c\uff0c\u60a8\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u4ee5\u4f7f\u7528\u5b83\u3002",
"install_submit_title": "\u606d\u559c\uff01",
"install_submit_desc": "\u8a72\u8a2d\u7f6e\u7a0b\u5e8f\u88ab\u5b8c\u6210\uff0c\u4e14\u60a8\u6e96\u5099\u597d\u958b\u59cb\u4f7f\u7528AdGuard Home\u3002",
"install_submit_desc": "\u8a72\u8a2d\u7f6e\u7a0b\u5e8f\u88ab\u5b8c\u6210\uff0c\u4e14\u60a8\u6e96\u5099\u597d\u958b\u59cb\u4f7f\u7528 AdGuard Home\u3002",
"install_devices_router": "\u8def\u7531\u5668",
"install_devices_router_desc": "\u8a72\u8a2d\u7f6e\u5c07\u81ea\u52d5\u5730\u6db5\u84cb\u88ab\u9023\u7dda\u81f3\u60a8\u7684\u5bb6\u5ead\u8def\u7531\u5668\u4e4b\u6240\u6709\u7684\u88dd\u7f6e\uff0c\u4e14\u60a8\u5c07\u7121\u9700\u624b\u52d5\u5730\u914d\u7f6e\u5b83\u5011\u6bcf\u500b\u3002",
"install_devices_address": "AdGuard Home DNS\u4f3a\u670d\u5668\u6b63\u5728\u76e3\u807d\u4e0b\u5217\u7684\u4f4d\u5740",
"install_devices_address": "AdGuard Home DNS \u4f3a\u670d\u5668\u6b63\u5728\u76e3\u807d\u4e0b\u5217\u7684\u4f4d\u5740",
"install_devices_router_list_1": "\u958b\u555f\u95dc\u65bc\u60a8\u7684\u8def\u7531\u5668\u4e4b\u504f\u597d\u8a2d\u5b9a\u3002\u901a\u5e38\u5730\uff0c\u60a8\u53ef\u900f\u904e\u7db2\u5740\uff08\u5982 http:\/\/192.168.0.1\/ \u6216 http:\/\/192.168.1.1\/\uff09\u5f9e\u60a8\u7684\u700f\u89bd\u5668\u4e2d\u5b58\u53d6\u5b83\u3002\u60a8\u53ef\u80fd\u88ab\u8981\u6c42\u8f38\u5165\u8a72\u5bc6\u78bc\u3002\u5982\u679c\u60a8\u4e0d\u8a18\u5f97\u5b83\uff0c\u60a8\u7d93\u5e38\u53ef\u900f\u904e\u6309\u58d3\u65bc\u8a72\u8def\u7531\u5668\u672c\u8eab\u4e0a\u7684\u6309\u9215\u4f86\u91cd\u7f6e\u5bc6\u78bc\u3002\u67d0\u4e9b\u8def\u7531\u5668\u9700\u8981\u7279\u5b9a\u7684\u61c9\u7528\u7a0b\u5f0f\uff0c\u65e2\u7136\u5982\u6b64\u5176\u61c9\u5df2\u88ab\u5b89\u88dd\u65bc\u60a8\u7684\u96fb\u8166\/\u624b\u6a5f\u4e0a\u3002",
"install_devices_router_list_2": "\u627e\u5230DHCP\/DNS\u8a2d\u5b9a\u3002\u5c0b\u627e\u7dca\u9130\u8457\u5141\u8a31\u5169\u7d44\u6216\u4e09\u7d44\u6578\u5b57\u96c6\u7684\u6b04\u4f4d\u4e4bDNS\u5b57\u6bcd\uff0c\u6bcf\u7d44\u88ab\u62c6\u6210\u56db\u500b\u542b\u6709\u4e00\u81f3\u4e09\u500b\u6578\u5b57\u7684\u7fa4\u96c6\u3002",
"install_devices_router_list_3": "\u5728\u90a3\u88e1\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_windows_list_1": "\u901a\u904e\u958b\u59cb\u529f\u80fd\u8868\u6216Windows \u641c\u5c0b\uff0c\u958b\u555f\u63a7\u5236\u53f0\u3002",
"install_devices_router_list_2": "\u627e\u5230 DHCP\/DNS \u8a2d\u5b9a\u3002\u5c0b\u627e\u7dca\u9130\u8457\u5141\u8a31\u5169\u7d44\u6216\u4e09\u7d44\u6578\u5b57\u96c6\u7684\u6b04\u4f4d\u4e4b DNS \u5b57\u6bcd\uff0c\u6bcf\u7d44\u88ab\u62c6\u6210\u56db\u500b\u542b\u6709\u4e00\u81f3\u4e09\u500b\u6578\u5b57\u7684\u7fa4\u96c6\u3002",
"install_devices_router_list_3": "\u5728\u90a3\u88e1\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_windows_list_1": "\u901a\u904e\u958b\u59cb\u529f\u80fd\u8868\u6216 Windows \u641c\u5c0b\uff0c\u958b\u555f\u63a7\u5236\u53f0\u3002",
"install_devices_windows_list_2": "\u53bb\u7db2\u8def\u548c\u7db2\u969b\u7db2\u8def\u985e\u5225\uff0c\u7136\u5f8c\u53bb\u7db2\u8def\u548c\u5171\u7528\u4e2d\u5fc3\u3002",
"install_devices_windows_list_3": "\u65bc\u756b\u9762\u4e4b\u5de6\u5074\u4e0a\u627e\u5230\u8b8a\u66f4\u4ecb\u9762\u5361\u8a2d\u5b9a\u4e26\u65bc\u5b83\u4e0a\u9ede\u64ca\u3002",
"install_devices_windows_list_4": "\u9078\u64c7\u60a8\u73fe\u884c\u7684\u9023\u7dda\uff0c\u65bc\u5b83\u4e0a\u9ede\u64ca\u6ed1\u9f20\u53f3\u9375\uff0c\u7136\u5f8c\u9078\u64c7\u5167\u5bb9\u3002",
"install_devices_windows_list_5": "\u5728\u6e05\u55ae\u4e2d\u627e\u5230\u7db2\u969b\u7db2\u8def\u901a\u8a0a\u5354\u5b9a\u7b2c 4 \u7248\uff08TCP\/IPv4\uff09\uff0c\u9078\u64c7\u5b83\uff0c\u7136\u5f8c\u518d\u6b21\u65bc\u5167\u5bb9\u4e0a\u9ede\u64ca\u3002",
"install_devices_windows_list_6": "\u9078\u64c7\u4f7f\u7528\u4e0b\u5217\u7684DNS\u4f3a\u670d\u5668\u4f4d\u5740\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_macos_list_1": "\u65bcApple\u5716\u50cf\u4e0a\u9ede\u64ca\uff0c\u7136\u5f8c\u53bb\u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a\u3002",
"install_devices_windows_list_6": "\u9078\u64c7\u4f7f\u7528\u4e0b\u5217\u7684 DNS \u4f3a\u670d\u5668\u4f4d\u5740\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_macos_list_1": "\u65bc Apple \u5716\u50cf\u4e0a\u9ede\u64ca\uff0c\u7136\u5f8c\u53bb\u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a\u3002",
"install_devices_macos_list_2": "\u65bc\u7db2\u8def\u4e0a\u9ede\u64ca\u3002",
"install_devices_macos_list_3": "\u9078\u64c7\u5728\u60a8\u7684\u6e05\u55ae\u4e2d\u4e4b\u9996\u8981\u7684\u9023\u7dda\uff0c\u7136\u5f8c\u9ede\u64ca\u9032\u968e\u7684\u3002",
"install_devices_macos_list_4": "\u9078\u64c7\u8a72DNS\u5206\u9801\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_android_list_1": "\u5f9eAndroid\u9078\u55ae\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
"install_devices_android_list_2": "\u65bc\u8a72\u9078\u55ae\u4e0a\u8f15\u89f8Wi-Fi\u3002\u6b63\u5728\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7db2\u8def\u4e4b\u756b\u9762\u5c07\u88ab\u986f\u793a\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u9023\u7dda\u8a2d\u5b9a\u81ea\u8a02\u7684DNS\uff09\u3002",
"install_devices_macos_list_4": "\u9078\u64c7\u8a72 DNS \u5206\u9801\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_android_list_1": "\u5f9e Android \u9078\u55ae\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
"install_devices_android_list_2": "\u65bc\u8a72\u9078\u55ae\u4e0a\u8f15\u89f8 Wi-Fi\u3002\u6b63\u5728\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7db2\u8def\u4e4b\u756b\u9762\u5c07\u88ab\u986f\u793a\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u9023\u7dda\u8a2d\u5b9a\u81ea\u8a02\u7684 DNS\uff09\u3002",
"install_devices_android_list_3": "\u9577\u6309\u60a8\u6240\u9023\u7dda\u81f3\u7684\u7db2\u8def\uff0c\u7136\u5f8c\u8f15\u89f8\u4fee\u6539\u7db2\u8def\u3002",
"install_devices_android_list_4": "\u65bc\u67d0\u4e9b\u88dd\u7f6e\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u6aa2\u67e5\u95dc\u65bc\u9032\u968e\u7684\u65b9\u6846\u4ee5\u67e5\u770b\u9032\u4e00\u6b65\u7684\u8a2d\u5b9a\u3002\u70ba\u4e86\u8abf\u6574\u60a8\u7684Android DNS\u8a2d\u5b9a\uff0c\u60a8\u5c07\u9700\u8981\u628aIP \u8a2d\u5b9a\u5f9eDHCP\u8f49\u63db\u6210\u975c\u614b\u3002",
"install_devices_android_list_5": "\u4f7f\u8a2d\u5b9aDNS 1\u548cDNS 2\u503c\u66f4\u6539\u6210\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_android_list_4": "\u65bc\u67d0\u4e9b\u88dd\u7f6e\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u6aa2\u67e5\u95dc\u65bc\u9032\u968e\u7684\u65b9\u6846\u4ee5\u67e5\u770b\u9032\u4e00\u6b65\u7684\u8a2d\u5b9a\u3002\u70ba\u4e86\u8abf\u6574\u60a8\u7684 Android DNS \u8a2d\u5b9a\uff0c\u60a8\u5c07\u9700\u8981\u628a IP \u8a2d\u5b9a\u5f9e DHCP \u8f49\u63db\u6210\u975c\u614b\u3002",
"install_devices_android_list_5": "\u4f7f\u8a2d\u5b9a DNS 1 \u548c DNS 2 \u503c\u66f4\u6539\u6210\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_ios_list_1": "\u5f9e\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
"install_devices_ios_list_2": "\u5728\u5de6\u5074\u7684\u9078\u55ae\u4e2d\u9078\u64c7Wi-Fi\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u7db2\u8def\u914d\u7f6eDNS\uff09\u3002",
"install_devices_ios_list_2": "\u5728\u5de6\u5074\u7684\u9078\u55ae\u4e2d\u9078\u64c7 Wi-Fi\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u7db2\u8def\u914d\u7f6e DNS\uff09\u3002",
"install_devices_ios_list_3": "\u65bc\u76ee\u524d\u73fe\u884c\u7684\u7db2\u8def\u4e4b\u540d\u7a31\u4e0a\u8f15\u89f8\u3002",
"install_devices_ios_list_4": "\u5728\u8a72DNS\u6b04\u4f4d\u4e2d\uff0c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_ios_list_4": "\u5728\u8a72 DNS \u6b04\u4f4d\u4e2d\uff0c\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"get_started": "\u958b\u59cb\u5427",
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
"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_desc": "\u52a0\u5bc6\uff08HTTPS\/TLS\uff09\u652f\u63f4\u4f9b DNS \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_server_desc": "\u70ba\u4e86\u4f7f\u7528 HTTPS\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\u65b0\u5c0e\u5411\u5230 HTTPS",
"encryption_redirect_desc": "\u5982\u679c\u88ab\u52fe\u9078\uff0cAdGuard Home \u5c07\u81ea\u52d5\u5730\u91cd\u65b0\u5c0e\u5411\u60a8\u5f9e HTTP \u5230 HTTPS \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_https_desc": "\u5982\u679c HTTPS \u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home \u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904e HTTPS \u5c07\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u4e14\u5b83\u4e5f\u5c07\u65bc '\/dns-query' \u4f4d\u7f6e\u4e0a\u63d0\u4f9b DNS-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_dot_desc": "\u5982\u679c\u8a72\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home \u5c07\u65bc\u6b64\u9023\u63a5\u57e0\u4e0a\u904b\u884c DNS-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",
@@ -232,8 +252,8 @@
"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_enable": "\u555f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS \u548c DNS-over-TLS\uff09",
"encryption_enable_desc": "\u5982\u679c\u52a0\u5bc6\u88ab\u555f\u7528\uff0cAdGuard Home \u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904e HTTPS \u5c07\u904b\u4f5c\uff0c\u4e14\u8a72 DNS \u4f3a\u670d\u5668\u5c07\u7559\u5fc3\u76e3\u807d\u900f\u904e DNS-over-HTTPS \u548c DNS-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",
@@ -244,12 +264,53 @@
"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_range": "\u5728 80-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"
"dns_addresses": "DNS \u4f4d\u5740",
"down": "\u505c\u6b62\u904b\u4f5c\u7684",
"fix": "\u4fee\u5fa9",
"dns_providers": "\u9019\u88e1\u662f\u4e00\u500b\u5f9e\u4e2d\u9078\u64c7\u4e4b<0>\u5df2\u77e5\u7684 DNS \u4f9b\u61c9\u5546\u4e4b\u6e05\u55ae<\/0>\u3002",
"update_now": "\u7acb\u5373\u66f4\u65b0",
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
"processing_update": "Please wait, AdGuard Home is being updated",
"clients_title": "\u7528\u6236\u7aef",
"clients_desc": "Configure devices connected to AdGuard Home",
"settings_global": "Global",
"settings_custom": "Custom",
"table_client": "\u7528\u6236\u7aef",
"table_name": "\u540d\u7a31",
"save_btn": "\u5132\u5b58",
"client_add": "\u589e\u52a0\u7528\u6236\u7aef",
"client_new": "\u65b0\u7684\u7528\u6236\u7aef",
"client_edit": "\u7de8\u8f2f\u7528\u6236\u7aef",
"client_identifier": "Identifier",
"ip_address": "IP \u4f4d\u5740",
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
"form_enter_ip": "\u8f38\u5165 IP",
"form_enter_mac": "\u8f38\u5165\u5a92\u9ad4\u5b58\u53d6\u63a7\u5236\uff08MAC\uff09",
"form_client_name": "\u8f38\u5165\u7528\u6236\u7aef\u540d\u7a31",
"client_global_settings": "Use global settings",
"client_deleted": "Client \"{{key}}\" successfully deleted",
"client_added": "Client \"{{key}}\" successfully added",
"client_updated": "Client \"{{key}}\" successfully updated",
"table_statistics": "Requests count (last 24 hours)",
"clients_not_found": "No clients found",
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
"filter_confirm_delete": "Are you sure you want to delete filter?",
"auto_clients_title": "Clients (runtime)",
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
"access_disallowed_title": "Disallowed clients",
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_blocked_title": "Blocked domains",
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
"access_settings_saved": "Access settings successfully saved"
}

View File

@@ -0,0 +1,45 @@
import { createAction } from 'redux-actions';
import Api from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { normalizeTextarea } from '../helpers/helpers';
const apiClient = new Api();
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');
export const getAccessList = () => async (dispatch) => {
dispatch(getAccessListRequest());
try {
const data = await apiClient.getAccessList();
dispatch(getAccessListSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getAccessListFailure());
}
};
export const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');
export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');
export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');
export const setAccessList = config => async (dispatch) => {
dispatch(setAccessListRequest());
try {
const { allowed_clients, disallowed_clients, blocked_hosts } = config;
const values = {
allowed_clients: (allowed_clients && normalizeTextarea(allowed_clients)) || [],
disallowed_clients: (disallowed_clients && normalizeTextarea(disallowed_clients)) || [],
blocked_hosts: (blocked_hosts && normalizeTextarea(blocked_hosts)) || [],
};
await apiClient.setAccessList(values);
dispatch(setAccessListSuccess());
dispatch(addSuccessToast('access_settings_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setAccessListFailure());
}
};

View File

@@ -0,0 +1,84 @@
import { createAction } from 'redux-actions';
import { t } from 'i18next';
import Api from '../api/Api';
import { addErrorToast, addSuccessToast, getClients } from './index';
import { CLIENT_ID } from '../helpers/constants';
const apiClient = new Api();
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
export const addClient = config => async (dispatch) => {
dispatch(addClientRequest());
try {
let data;
if (config.identifier === CLIENT_ID.MAC) {
const { ip, identifier, ...values } = config;
data = { ...values };
} else {
const { mac, identifier, ...values } = config;
data = { ...values };
}
await apiClient.addClient(data);
dispatch(addClientSuccess());
dispatch(toggleClientModal());
dispatch(addSuccessToast(t('client_added', { key: config.name })));
dispatch(getClients());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addClientFailure());
}
};
export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
export const deleteClient = config => async (dispatch) => {
dispatch(deleteClientRequest());
try {
await apiClient.deleteClient(config);
dispatch(deleteClientSuccess());
dispatch(addSuccessToast(t('client_deleted', { key: config.name })));
dispatch(getClients());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(deleteClientFailure());
}
};
export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
export const updateClient = (config, name) => async (dispatch) => {
dispatch(updateClientRequest());
try {
let data;
if (config.identifier === CLIENT_ID.MAC) {
const { ip, identifier, ...values } = config;
data = { name, data: { ...values } };
} else {
const { mac, identifier, ...values } = config;
data = { name, data: { ...values } };
}
await apiClient.updateClient(data);
dispatch(updateClientSuccess());
dispatch(toggleClientModal());
dispatch(addSuccessToast(t('client_updated', { key: name })));
dispatch(getClients());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(updateClientFailure());
}
};

View File

@@ -2,15 +2,17 @@ import { createAction } from 'redux-actions';
import round from 'lodash/round';
import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import axios from 'axios';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
import { SETTINGS_NAMES } from '../helpers/constants';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers';
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
import Api from '../api/Api';
const apiClient = new Api();
export const addErrorToast = createAction('ADD_ERROR_TOAST');
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
export const addNoticeToast = createAction('ADD_NOTICE_TOAST');
export const removeToast = createAction('REMOVE_TOAST');
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
@@ -139,6 +141,119 @@ 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 getUpdateRequest = createAction('GET_UPDATE_REQUEST');
export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
export const getUpdate = () => async (dispatch, getState) => {
const { dnsVersion } = getState().dashboard;
dispatch(getUpdateRequest());
try {
await apiClient.getUpdate();
const checkUpdate = async (attempts) => {
let count = attempts || 1;
let timeout;
if (count > 60) {
dispatch(addNoticeToast({ error: 'update_failed' }));
dispatch(getUpdateFailure());
return false;
}
const rmTimeout = t => t && clearTimeout(t);
const setRecursiveTimeout = (time, ...args) => setTimeout(
checkUpdate,
time,
...args,
);
axios.get('control/status')
.then((response) => {
rmTimeout(timeout);
if (response && response.status === 200) {
const responseVersion = response.data && response.data.version;
if (dnsVersion !== responseVersion) {
dispatch(getUpdateSuccess());
window.location.reload(true);
}
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
})
.catch(() => {
rmTimeout(timeout);
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
});
return false;
};
checkUpdate();
} catch (error) {
dispatch(addNoticeToast({ error: 'update_failed' }));
dispatch(getUpdateFailure());
}
};
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 data = await apiClient.getClients();
const sortedClients = data.clients && sortClients(data.clients);
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
dispatch(getClientsSuccess({
clients: sortedClients || [],
autoClients: sortedAutoClients || [],
}));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getClientsFailure());
}
};
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest());
const timer = setInterval(async () => {
const state = getState();
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}
}, 100);
};
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
@@ -148,6 +263,9 @@ export const getDnsStatus = () => async (dispatch) => {
try {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus));
dispatch(getVersion());
dispatch(getClients());
dispatch(getTopStats());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
@@ -205,42 +323,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');
export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest());
const timer = setInterval(async () => {
const state = getState();
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}
}, 100);
};
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
@@ -586,41 +668,18 @@ export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
// TODO rewrite findActiveDhcp part
export const setDhcpConfig = values => async (dispatch, getState) => {
const { config } = getState().dhcp;
const updatedConfig = { ...config, ...values };
dispatch(setDhcpConfigRequest());
if (values.interface_name) {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(values.interface_name);
dispatch(findActiveDhcpSuccess(activeDhcp));
if (!activeDhcp.found) {
try {
await apiClient.setDhcpConfig(updatedConfig);
dispatch(setDhcpConfigSuccess(updatedConfig));
dispatch(addSuccessToast('dhcp_config_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
}
} else {
try {
await apiClient.setDhcpConfig(updatedConfig);
dispatch(setDhcpConfigSuccess(updatedConfig));
dispatch(addSuccessToast('dhcp_config_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
dispatch(findActiveDhcp(values.interface_name));
try {
await apiClient.setDhcpConfig(updatedConfig);
dispatch(setDhcpConfigSuccess(updatedConfig));
dispatch(addSuccessToast('dhcp_config_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
};
@@ -628,55 +687,60 @@ export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
// TODO rewrite findActiveDhcp part
export const toggleDhcp = config => async (dispatch) => {
export const toggleDhcp = values => async (dispatch) => {
dispatch(toggleDhcpRequest());
let config = { ...values, enabled: false };
let successMessage = 'disabled_dhcp';
if (config.enabled) {
try {
await apiClient.setDhcpConfig({ ...config, enabled: false });
dispatch(toggleDhcpSuccess());
dispatch(addSuccessToast('disabled_dhcp'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
}
} else {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
dispatch(findActiveDhcpSuccess(activeDhcp));
if (!activeDhcp.found) {
try {
await apiClient.setDhcpConfig({ ...config, enabled: true });
dispatch(toggleDhcpSuccess());
dispatch(addSuccessToast('enabled_dhcp'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
}
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
}
if (!values.enabled) {
config = { ...values, enabled: true };
successMessage = 'enabled_dhcp';
dispatch(findActiveDhcp(values.interface_name));
}
};
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));
await apiClient.setDhcpConfig(config);
dispatch(toggleDhcpSuccess());
dispatch(addSuccessToast(successMessage));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getClientsFailure());
dispatch(toggleDhcpFailure());
}
};
export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');
export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');
export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE');
export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');
export const addStaticLease = config => async (dispatch) => {
dispatch(addStaticLeaseRequest());
try {
const name = config.hostname || config.ip;
await apiClient.addStaticLease(config);
dispatch(addStaticLeaseSuccess(config));
dispatch(addSuccessToast(t('dhcp_lease_added', { key: name })));
dispatch(toggleLeaseModal());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addStaticLeaseFailure());
}
};
export const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUEST');
export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE');
export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');
export const removeStaticLease = config => async (dispatch) => {
dispatch(removeStaticLeaseRequest());
try {
const name = config.hostname || config.ip;
await apiClient.removeStaticLease(config);
dispatch(removeStaticLeaseSuccess(config));
dispatch(addSuccessToast(t('dhcp_lease_deleted', { key: name })));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(removeStaticLeaseFailure());
}
};

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

@@ -39,7 +39,7 @@ export default class Api {
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' }
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
restartGlobalFiltering() {
const { path, method } = this.GLOBAL_RESTART;
@@ -140,8 +140,8 @@ export default class Api {
return this.makeRequest(path, method);
}
getGlobalClients() {
const { path, method } = this.GLOBAL_CLIENTS;
getUpdate() {
const { path, method } = this.GLOBAL_UPDATE;
return this.makeRequest(path, method);
}
@@ -149,9 +149,9 @@ export default class Api {
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' };
@@ -188,15 +188,14 @@ export default class Api {
return this.makeRequest(path, method, config);
}
removeFilter(url) {
removeFilter(config) {
const { path, method } = this.FILTERING_REMOVE_FILTER;
const parameter = 'url';
const requestBody = `${parameter}=${url}`;
const config = {
data: requestBody,
header: { 'Content-Type': 'text/plain' },
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
return this.makeRequest(path, method, parameters);
}
setRules(rules) {
@@ -318,6 +317,8 @@ export default class Api {
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
getDhcpStatus() {
const { path, method } = this.DHCP_STATUS;
@@ -347,9 +348,28 @@ export default class Api {
return this.makeRequest(path, method, parameters);
}
addStaticLease(config) {
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
removeStaticLease(config) {
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
// 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;
@@ -365,6 +385,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' };
@@ -392,4 +421,60 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
// Per-client settings
GET_CLIENTS = { path: 'clients', method: 'GET' };
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
getClients() {
const { path, method } = this.GET_CLIENTS;
return this.makeRequest(path, method);
}
addClient(config) {
const { path, method } = this.ADD_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
deleteClient(config) {
const { path, method } = this.DELETE_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
updateClient(config) {
const { path, method } = this.UPDATE_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
// DNS access settings
ACCESS_LIST = { path: 'access/list', method: 'GET' };
ACCESS_SET = { path: 'access/set', method: 'POST' };
getAccessList() {
const { path, method } = this.ACCESS_LIST;
return this.makeRequest(path, method);
}
setAccessList(config) {
const { path, method } = this.ACCESS_SET;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
}

View File

@@ -13,20 +13,26 @@ import Header from '../../containers/Header';
import Dashboard from '../../containers/Dashboard';
import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import Dns from '../../containers/Dns';
import Encryption from '../../containers/Encryption';
import Dhcp from '../../containers/Dhcp';
import Clients from '../../containers/Clients';
import Logs from '../../containers/Logs';
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 UpdateOverlay from '../ui/UpdateOverlay';
import EncryptionTopline from '../ui/EncryptionTopline';
import Icons from '../ui/Icons';
import i18n from '../../i18n';
class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
this.props.getVersion();
this.props.getClients();
}
componentDidUpdate(prevProps) {
@@ -39,6 +45,10 @@ class App extends Component {
this.props.enableDns();
};
handleUpdate = () => {
this.props.getUpdate();
};
setLanguage = () => {
const { processing, language } = this.props.dashboard;
@@ -51,49 +61,58 @@ class App extends Component {
i18n.on('languageChanged', (lang) => {
this.props.changeLanguage(lang);
});
}
};
render() {
const { dashboard, encryption } = this.props;
const updateAvailable =
!dashboard.processingVersions &&
dashboard.isCoreRunning &&
dashboard.isUpdateAvailable;
!dashboard.processingVersions && dashboard.isCoreRunning && dashboard.isUpdateAvailable;
return (
<HashRouter hashType='noslash'>
<HashRouter hashType="noslash">
<Fragment>
{updateAvailable &&
<UpdateTopline
url={dashboard.announcementUrl}
version={dashboard.version}
/>
}
{!encryption.processing &&
{updateAvailable && (
<Fragment>
<UpdateTopline
url={dashboard.announcementUrl}
version={dashboard.newVersion}
canAutoUpdate={dashboard.canAutoUpdate}
getUpdate={this.handleUpdate}
processingUpdate={dashboard.processingUpdate}
/>
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
</Fragment>
)}
{!encryption.processing && (
<EncryptionTopline notAfter={encryption.not_after} />
}
)}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
{!dashboard.processing && !dashboard.isCoreRunning &&
{!dashboard.processing && !dashboard.isCoreRunning && (
<div className="row row-cards">
<div className="col-lg-12">
<Status handleStatusChange={this.handleStatusChange} />
</div>
</div>
}
{!dashboard.processing && dashboard.isCoreRunning &&
)}
{!dashboard.processing && dashboard.isCoreRunning && (
<Fragment>
<Route path="/" exact component={Dashboard} />
<Route path="/settings" component={Settings} />
<Route path="/dns" component={Dns} />
<Route path="/encryption" component={Encryption} />
<Route path="/dhcp" component={Dhcp} />
<Route path="/clients" component={Clients} />
<Route path="/filters" component={Filters} />
<Route path="/logs" component={Logs} />
<Route path="/guide" component={SetupGuide} />
</Fragment>
}
)}
</div>
<Footer />
<Toasts />
<Icons />
</Fragment>
</HashRouter>
);
@@ -102,14 +121,13 @@ class App extends Component {
App.propTypes = {
getDnsStatus: PropTypes.func,
getUpdate: PropTypes.func,
enableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
getClients: PropTypes.func,
};
export default withNamespaces()(App);

View File

@@ -24,7 +24,8 @@ class Clients extends Component {
Header: 'IP',
accessor: 'ip',
Cell: ({ value }) => {
const clientName = getClientName(this.props.clients, value);
const clientName = getClientName(this.props.clients, value)
|| getClientName(this.props.autoClients, value);
let client;
if (clientName) {
@@ -79,6 +80,7 @@ Clients.propTypes = {
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
clients: PropTypes.array.isRequired,
autoClients: PropTypes.array.isRequired,
t: PropTypes.func,
};

View File

@@ -21,6 +21,7 @@ class Dashboard extends Component {
this.props.getStats();
this.props.getStatsHistory();
this.props.getTopStats();
this.props.getClients();
}
getToggleFilteringButton = () => {
@@ -96,6 +97,7 @@ class Dashboard extends Component {
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
clients={dashboard.clients}
autoClients={dashboard.autoClients}
/>
</div>
<div className="col-lg-6">
@@ -131,6 +133,7 @@ Dashboard.propTypes = {
isCoreRunning: PropTypes.bool,
getFiltering: PropTypes.func,
toggleProtection: PropTypes.func,
getClients: PropTypes.func,
processingProtection: PropTypes.bool,
t: PropTypes.func,
};

View File

@@ -1,13 +0,0 @@
.remove-icon {
position: relative;
top: 2px;
display: inline-block;
width: 20px;
height: 18px;
opacity: 0.6;
}
.remove-icon:hover {
cursor: pointer;
opacity: 1;
}

View File

@@ -6,7 +6,6 @@ import Modal from '../ui/Modal';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import UserRules from './UserRules';
import './Filters.css';
class Filters extends Component {
componentDidMount() {
@@ -33,6 +32,13 @@ class Filters extends Component {
);
};
handleDelete = (url) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('filter_confirm_delete'))) {
this.props.removeFilter({ url });
}
}
columns = [{
Header: <Trans>enabled_table_header</Trans>,
accessor: 'enabled',
@@ -59,7 +65,18 @@ class Filters extends Component {
}, {
Header: <Trans>actions_table_header</Trans>,
accessor: 'url',
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
Cell: ({ value }) => (
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.handleDelete(value)}
title={this.props.t('delete_table_action')}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
),
className: 'text-center',
width: 80,
sortable: false,

View File

@@ -16,11 +16,13 @@
stroke: #9aa0ac;
}
.nav-tabs .nav-link.active .nav-icon {
.nav-tabs .nav-link.active .nav-icon,
.nav-tabs .nav-item.show .nav-icon {
stroke: #66b574;
}
.nav-tabs .nav-link.active:hover .nav-icon {
.nav-tabs .nav-link.active:hover .nav-icon,
.nav-tabs .nav-item.show:hover .nav-icon {
stroke: #58a273;
}
@@ -87,6 +89,12 @@
height: 32px;
}
.nav-tabs .nav-item.show .nav-link {
color: #66b574;
background-color: #fff;
border-bottom-color: #66b574;
}
@media screen and (min-width: 992px) {
.header {
padding: 0;
@@ -117,6 +125,10 @@
.nav-version {
padding: 0;
}
.nav-icon {
display: none;
}
}
@media screen and (min-width: 1280px) {
@@ -127,6 +139,10 @@
.nav-version {
font-size: 0.85rem;
}
.nav-icon {
display: block;
}
}
.dns-status {

View File

@@ -5,6 +5,9 @@ import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import { SETTINGS_URLS } from '../../helpers/constants';
import Dropdown from '../ui/Dropdown';
class Menu extends Component {
handleClickOutside = () => {
this.props.closeMenu();
@@ -14,49 +17,86 @@ class Menu extends Component {
this.props.toggleMenuOpen();
};
getActiveClassForSettings = () => {
const { pathname } = this.props.location;
const isSettingsPage = SETTINGS_URLS.some(item => item === pathname);
return isSettingsPage ? 'active' : '';
};
render() {
const menuClass = classnames({
'col-lg-6 mobile-menu': true,
'mobile-menu--active': this.props.isMenuOpen,
});
const dropdownControlClass = `nav-link ${this.getActiveClassForSettings()}`;
return (
<Fragment>
<div className={menuClass}>
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
<li className="nav-item border-bottom d-lg-none" onClick={this.toggleMenu}>
<div className="nav-link nav-link--back">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/></svg>
<svg className="nav-icon">
<use xlinkHref="#back" />
</svg>
<Trans>back</Trans>
</div>
</li>
<li className="nav-item">
<NavLink to="/" exact={true} className="nav-link">
<svg className="nav-icon" fill="none" height="24" stroke="#9aa0ac" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/></svg>
<svg className="nav-icon">
<use xlinkHref="#dashboard" />
</svg>
<Trans>dashboard</Trans>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/settings" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/></svg>
<Trans>settings</Trans>
</NavLink>
</li>
<Dropdown
label={this.props.t('settings')}
baseClassName="dropdown nav-item"
controlClassName={dropdownControlClass}
icon="settings"
>
<Fragment>
<NavLink to="/settings" className="dropdown-item">
<Trans>general_settings</Trans>
</NavLink>
<NavLink to="/dns" className="dropdown-item">
<Trans>dns_settings</Trans>
</NavLink>
<NavLink to="/encryption" className="dropdown-item">
<Trans>encryption_settings</Trans>
</NavLink>
<NavLink to="/clients" className="dropdown-item">
<Trans>client_settings</Trans>
</NavLink>
<NavLink to="/dhcp" className="dropdown-item">
<Trans>dhcp_settings</Trans>
</NavLink>
</Fragment>
</Dropdown>
<li className="nav-item">
<NavLink to="/filters" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/></svg>
<svg className="nav-icon">
<use xlinkHref="#filters" />
</svg>
<Trans>filters</Trans>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/logs" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/></svg>
<svg className="nav-icon">
<use xlinkHref="#log" />
</svg>
<Trans>query_log</Trans>
</NavLink>
</li>
<li className="nav-item">
<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>
<NavLink to="/guide" className="nav-link">
<svg className="nav-icon">
<use xlinkHref="#setup" />
</svg>
<Trans>setup_guide</Trans>
</NavLink>
</li>
@@ -71,6 +111,8 @@ Menu.propTypes = {
isMenuOpen: PropTypes.bool,
closeMenu: PropTypes.func,
toggleMenuOpen: PropTypes.func,
location: PropTypes.object,
t: PropTypes.func,
};
export default withNamespaces()(enhanceWithClickOutside(Menu));

View File

@@ -5,6 +5,10 @@
min-height: 26px;
}
.logs__row--center {
justify-content: center;
}
.logs__row--overflow {
overflow: hidden;
}

View File

@@ -196,7 +196,8 @@ 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);
const clientName = getClientName(dashboard.clients, row.value)
|| getClientName(dashboard.autoClients, row.value);
let client;
if (clientName) {

View File

@@ -0,0 +1,118 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import ReactTable from 'react-table';
import { CLIENT_ID } from '../../../helpers/constants';
import Card from '../../ui/Card';
class AutoClients extends Component {
getClient = (name, clients) => {
const client = clients.find(item => name === item.name);
if (client) {
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
return {
identifier,
use_global_settings: true,
...client,
};
}
return {
identifier: 'ip',
use_global_settings: true,
};
};
getStats = (ip, stats) => {
if (stats && stats.top_clients) {
return stats.top_clients[ip];
}
return '';
};
cellWrap = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
columns = [
{
Header: this.props.t('table_client'),
accessor: 'ip',
Cell: this.cellWrap,
},
{
Header: this.props.t('table_name'),
accessor: 'name',
Cell: this.cellWrap,
},
{
Header: this.props.t('source_label'),
accessor: 'source',
Cell: this.cellWrap,
},
{
Header: this.props.t('table_statistics'),
accessor: 'statistics',
Cell: (row) => {
const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
if (clientStats) {
return (
<div className="logs__row">
<div className="logs__text" title={clientStats}>
{clientStats}
</div>
</div>
);
}
return '';
},
},
];
render() {
const { t, autoClients } = this.props;
return (
<Card
title={t('auto_clients_title')}
subtitle={t('auto_clients_desc')}
bodyType="card-body box-body--settings"
>
<ReactTable
data={autoClients || []}
columns={this.columns}
className="-striped -highlight card-table-overflow"
showPagination={true}
defaultPageSize={10}
minRows={5}
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
rowsText={t('rows_table_footer_text')}
noDataText={t('clients_not_found')}
/>
</Card>
);
}
}
AutoClients.propTypes = {
t: PropTypes.func.isRequired,
autoClients: PropTypes.array.isRequired,
topStats: PropTypes.object.isRequired,
};
export default withNamespaces()(AutoClients);

View File

@@ -0,0 +1,260 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import ReactTable from 'react-table';
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
import Card from '../../ui/Card';
import Modal from './Modal';
class ClientsTable extends Component {
handleFormAdd = (values) => {
this.props.addClient(values);
};
handleFormUpdate = (values, name) => {
this.props.updateClient(values, name);
};
handleSubmit = (values) => {
if (this.props.modalType === MODAL_TYPE.EDIT) {
this.handleFormUpdate(values, this.props.modalClientName);
} else {
this.handleFormAdd(values);
}
};
cellWrap = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
getClient = (name, clients) => {
const client = clients.find(item => name === item.name);
if (client) {
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
return {
identifier,
use_global_settings: true,
...client,
};
}
return {
identifier: CLIENT_ID.IP,
use_global_settings: true,
};
};
getStats = (ip, stats) => {
if (stats && stats.top_clients) {
return stats.top_clients[ip];
}
return '';
};
handleDelete = (data) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
this.props.deleteClient(data);
}
};
columns = [
{
Header: this.props.t('table_client'),
accessor: 'ip',
Cell: (row) => {
if (row.original && row.original.mac) {
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={row.original.mac}>
{row.original.mac} <em>(MAC)</em>
</span>
</div>
);
} else if (row.value) {
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={row.value}>
{row.value} <em>(IP)</em>
</span>
</div>
);
}
return '';
},
},
{
Header: this.props.t('table_name'),
accessor: 'name',
Cell: this.cellWrap,
},
{
Header: this.props.t('settings'),
accessor: 'use_global_settings',
Cell: ({ value }) => {
const title = value ? (
<Trans>settings_global</Trans>
) : (
<Trans>settings_custom</Trans>
);
return (
<div className="logs__row logs__row--overflow">
<div className="logs__text" title={title}>
{title}
</div>
</div>
);
},
},
{
Header: this.props.t('table_statistics'),
accessor: 'statistics',
Cell: (row) => {
const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
if (clientStats) {
return (
<div className="logs__row">
<div className="logs__text" title={clientStats}>
{clientStats}
</div>
</div>
);
}
return '';
},
},
{
Header: this.props.t('actions_table_header'),
accessor: 'actions',
maxWidth: 150,
Cell: (row) => {
const clientName = row.original.name;
const {
toggleClientModal, processingDeleting, processingUpdating, t,
} = this.props;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() =>
toggleClientModal({
type: MODAL_TYPE.EDIT,
name: clientName,
})
}
disabled={processingUpdating}
title={t('edit_table_action')}
>
<svg className="icons">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.handleDelete({ name: clientName })}
disabled={processingDeleting}
title={t('delete_table_action')}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
);
},
},
];
render() {
const {
t,
clients,
isModalOpen,
modalType,
modalClientName,
toggleClientModal,
processingAdding,
processingUpdating,
} = this.props;
const currentClientData = this.getClient(modalClientName, clients);
return (
<Card
title={t('clients_title')}
subtitle={t('clients_desc')}
bodyType="card-body box-body--settings"
>
<Fragment>
<ReactTable
data={clients || []}
columns={this.columns}
className="-striped -highlight card-table-overflow"
showPagination={true}
defaultPageSize={10}
minRows={5}
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
rowsText={t('rows_table_footer_text')}
noDataText={t('clients_not_found')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
disabled={processingAdding}
>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
toggleClientModal={toggleClientModal}
currentClientData={currentClientData}
handleSubmit={this.handleSubmit}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
/>
</Fragment>
</Card>
);
}
}
ClientsTable.propTypes = {
t: PropTypes.func.isRequired,
clients: PropTypes.array.isRequired,
topStats: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired,
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
modalClientName: PropTypes.string.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
};
export default withNamespaces()(ClientsTable);

View File

@@ -0,0 +1,217 @@
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 { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
import { CLIENT_ID } from '../../../helpers/constants';
let Form = (props) => {
const {
t,
handleSubmit,
reset,
pristine,
submitting,
clientIdentifier,
useGlobalSettings,
toggleClientModal,
processingAdding,
processingUpdating,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group">
<div className="form-inline mb-3">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
<label className="mr-3">
<Field
name="identifier"
component={renderField}
type="radio"
className="form-control mr-2"
value="ip"
/>{' '}
<Trans>ip_address</Trans>
</label>
<label>
<Field
name="identifier"
component={renderField}
type="radio"
className="form-control mr-2"
value="mac"
/>{' '}
MAC
</label>
</div>
{clientIdentifier === CLIENT_ID.IP && (
<div className="form__group">
<Field
id="ip"
name="ip"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
/>
</div>
)}
{clientIdentifier === CLIENT_ID.MAC && (
<div className="form__group">
<Field
id="mac"
name="mac"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_mac')}
validate={[mac, required]}
/>
</div>
)}
<div className="form__desc">
<Trans
components={[
<a href="#settings_dhcp" key="0">
link
</a>,
]}
>
client_identifier_desc
</Trans>
</div>
</div>
<div className="form__group">
<Field
id="name"
name="name"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
validate={[required]}
/>
</div>
<div className="mb-4">
<strong>
<Trans>settings</Trans>
</strong>
</div>
<div className="form__group">
<Field
name="use_global_settings"
type="checkbox"
component={renderSelectField}
placeholder={t('client_global_settings')}
/>
</div>
<div className="form__group">
<Field
name="filtering_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('block_domain_use_filters_and_hosts')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="safebrowsing_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('use_adguard_browsing_sec')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="parental_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('use_adguard_parental')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="safesearch_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
toggleClientModal();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdding || processingUpdating}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
toggleClientModal: PropTypes.func.isRequired,
clientIdentifier: PropTypes.string,
useGlobalSettings: PropTypes.bool,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
};
const selector = formValueSelector('clientForm');
Form = connect((state) => {
const clientIdentifier = selector(state, 'identifier');
const useGlobalSettings = selector(state, 'use_global_settings');
return {
clientIdentifier,
useGlobalSettings,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'clientForm',
enableReinitialize: true,
}),
])(Form);

View File

@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form';
const Modal = (props) => {
const {
isModalOpen,
modalType,
currentClientData,
handleSubmit,
toggleClientModal,
processingAdding,
processingUpdating,
} = props;
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={() => toggleClientModal()}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT ? (
<Trans>client_edit</Trans>
) : (
<Trans>client_new</Trans>
)}
</h4>
<button type="button" className="close" onClick={() => toggleClientModal()}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{
...currentClientData,
}}
onSubmit={handleSubmit}
toggleClientModal={toggleClientModal}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
/>
</div>
</ReactModal>
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
currentClientData: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
toggleClientModal: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
};
export default withNamespaces()(Modal);

View File

@@ -0,0 +1,69 @@
import React, { Component, Fragment } from 'react';
import { withNamespaces } from 'react-i18next';
import PropTypes from 'prop-types';
import ClientsTable from './ClientsTable';
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Clients extends Component {
componentDidMount() {
this.props.getClients();
}
render() {
const {
t,
dashboard,
clients,
addClient,
updateClient,
deleteClient,
toggleClientModal,
} = this.props;
return (
<Fragment>
<PageTitle title={t('client_settings')} />
{(dashboard.processingTopStats || dashboard.processingClients) && <Loading />}
{!dashboard.processingTopStats && !dashboard.processingClients && (
<Fragment>
<ClientsTable
clients={dashboard.clients}
topStats={dashboard.topStats}
isModalOpen={clients.isModalOpen}
modalClientName={clients.modalClientName}
modalType={clients.modalType}
addClient={addClient}
updateClient={updateClient}
deleteClient={deleteClient}
toggleClientModal={toggleClientModal}
processingAdding={clients.processingAdding}
processingDeleting={clients.processingDeleting}
processingUpdating={clients.processingUpdating}
/>
<AutoClients
autoClients={dashboard.autoClients}
topStats={dashboard.topStats}
/>
</Fragment>
)}
</Fragment>
);
}
}
Clients.propTypes = {
t: PropTypes.func.isRequired,
dashboard: PropTypes.object.isRequired,
clients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
topStats: PropTypes.object,
};
export default withNamespaces()(Clients);

View File

@@ -1,22 +1,97 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces } from 'react-i18next';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
const Form = (props) => {
const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
let interfaceIP = option.ip_addresses[0];
if (!onlyIPv6) {
option.ip_addresses.forEach((ip) => {
if (!ip.includes(':')) {
interfaceIP = ip;
}
});
}
return (
<option value={name} key={name} disabled={onlyIPv6}>
{name} - {interfaceIP}
</option>
);
})
));
const renderInterfaceValues = (interfaceValues => (
<ul className="list-unstyled mt-1 mb-0">
<li>
<span className="interface__title">MTU: </span>
{interfaceValues.mtu}
</li>
<li>
<span className="interface__title"><Trans>dhcp_hardware_address</Trans>: </span>
{interfaceValues.hardware_address}
</li>
<li>
<span className="interface__title"><Trans>dhcp_ip_addresses</Trans>: </span>
{
interfaceValues.ip_addresses
.map(ip => <span key={ip} className="interface__ip">{ip}</span>)
}
</li>
</ul>
));
let Form = (props) => {
const {
t,
handleSubmit,
submitting,
invalid,
enabled,
interfaces,
interfaceValue,
processingConfig,
processingInterfaces,
} = props;
return (
<form onSubmit={handleSubmit}>
{!processingInterfaces && interfaces &&
<div className="row">
<div className="col-sm-12 col-md-6">
<div className="form__group form__group--settings">
<label>{t('dhcp_interface_select')}</label>
<Field
name="interface_name"
component="select"
className="form-control custom-select"
validate={[required]}
>
<option value="" disabled={enabled}>
{t('dhcp_interface_select')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
</div>
{interfaceValue &&
<div className="col-sm-12 col-md-6">
{interfaces[interfaceValue] &&
renderInterfaceValues(interfaces[interfaceValue])}
</div>
}
</div>
}
<hr/>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
@@ -101,11 +176,24 @@ Form.propTypes = {
submitting: PropTypes.bool,
invalid: PropTypes.bool,
interfaces: PropTypes.object,
interfaceValue: PropTypes.string,
initialValues: PropTypes.object,
processingConfig: PropTypes.bool,
processingInterfaces: PropTypes.bool,
enabled: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('dhcpForm');
Form = connect((state) => {
const interfaceValue = selector(state, 'interface_name');
return {
interfaceValue,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({ form: 'dhcpForm' }),

View File

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

View File

@@ -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

@@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { renderField, ipv4, mac, required } from '../../../../helpers/form';
const Form = (props) => {
const {
t,
handleSubmit,
reset,
pristine,
submitting,
toggleLeaseModal,
processingAdding,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group">
<Field
id="mac"
name="mac"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_mac')}
validate={[required, mac]}
/>
</div>
<div className="form__group">
<Field
id="ip"
name="ip"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_ip')}
validate={[required, ipv4]}
/>
</div>
<div className="form__group">
<Field
id="hostname"
name="hostname"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_hostname')}
/>
</div>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
toggleLeaseModal();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdding}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
toggleLeaseModal: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({ form: 'leaseForm' }),
])(Form);

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import ReactModal from 'react-modal';
import Form from './Form';
const Modal = (props) => {
const {
isModalOpen,
handleSubmit,
toggleLeaseModal,
processingAdding,
} = props;
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={() => toggleLeaseModal()}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
<Trans>dhcp_new_static_lease</Trans>
</h4>
<button type="button" className="close" onClick={() => toggleLeaseModal()}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
onSubmit={handleSubmit}
toggleLeaseModal={toggleLeaseModal}
processingAdding={processingAdding}
/>
</div>
</ReactModal>
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
toggleLeaseModal: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
};
export default withNamespaces()(Modal);

View File

@@ -0,0 +1,112 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withNamespaces } from 'react-i18next';
import Modal from './Modal';
class StaticLeases extends Component {
cellWrap = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}>
{value}
</span>
</div>
);
handleSubmit = (data) => {
this.props.addStaticLease(data);
}
handleDelete = (ip, mac, hostname = '') => {
const name = hostname || ip;
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('delete_confirm', { key: name }))) {
this.props.removeStaticLease({ ip, mac, hostname });
}
}
render() {
const {
isModalOpen,
toggleLeaseModal,
processingAdding,
processingDeleting,
staticLeases,
t,
} = this.props;
return (
<Fragment>
<ReactTable
data={staticLeases || []}
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>actions_table_header</Trans>,
accessor: 'actions',
maxWidth: 150,
Cell: (row) => {
const { ip, mac, hostname } = row.original;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
title={t('delete_table_action')}
disabled={processingDeleting}
onClick={() =>
this.handleDelete(ip, mac, hostname)
}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
);
},
},
]}
showPagination={false}
noDataText={t('dhcp_static_leases_not_found')}
className="-striped -highlight card-table-overflow"
minRows={6}
/>
<Modal
isModalOpen={isModalOpen}
toggleLeaseModal={toggleLeaseModal}
handleSubmit={this.handleSubmit}
processingAdding={processingAdding}
/>
</Fragment>
);
}
}
StaticLeases.propTypes = {
staticLeases: PropTypes.array.isRequired,
isModalOpen: PropTypes.bool.isRequired,
toggleLeaseModal: PropTypes.func.isRequired,
removeStaticLease: PropTypes.func.isRequired,
addStaticLease: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(StaticLeases);

View File

@@ -3,27 +3,39 @@ 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 StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import Accordion from '../../ui/Accordion';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Dhcp extends Component {
componentDidMount() {
this.props.getDhcpStatus();
this.props.getDhcpInterfaces();
}
handleFormSubmit = (values) => {
this.props.setDhcpConfig(values);
if (values.interface_name) {
this.props.setDhcpConfig(values);
}
};
handleToggle = (config) => {
this.props.toggleDhcp(config);
}
};
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;
}
@@ -49,46 +61,97 @@ class Dhcp extends Component {
className="btn btn-standard mr-2 btn-success"
onClick={() => this.handleToggle(config)}
disabled={
!filledConfig
|| activeDhcpFound
|| processingDhcp
|| processingConfig
!filledConfig || !check || otherDhcpFound || processingDhcp || processingConfig
}
>
<Trans>dhcp_enable</Trans>
</button>
);
}
};
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 '';
}
};
render() {
const { t, dhcp } = this.props;
@@ -96,65 +159,101 @@ class Dhcp extends Component {
'btn btn-primary btn-standard': true,
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
});
const {
enabled,
interface_name,
...values
} = dhcp.config;
const { enabled, interface_name, ...values } = dhcp.config;
return (
<Fragment>
<Card title={ t('dhcp_title') } subtitle={ t('dhcp_description') } bodyType="card-body box-body--settings">
<div className="dhcp">
{!dhcp.processing &&
<Fragment>
<Interface
onChange={this.handleFormSubmit}
initialValues={{ interface_name }}
interfaces={dhcp.interfaces}
processing={dhcp.processingInterfaces}
enabled={dhcp.config.enabled}
/>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{ ...values }}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
/>
<hr/>
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(dhcp.config.interface_name)
}
disabled={
!dhcp.config.interface_name
|| dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{this.getActiveDhcpMessage()}
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
</Fragment>
}
</div>
</Card>
{!dhcp.processing && dhcp.config.enabled &&
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
<div className="row">
<div className="col">
<Leases leases={dhcp.leases} />
<PageTitle title={t('dhcp_settings')} />
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
{!dhcp.processing && !dhcp.processingInterfaces && (
<Fragment>
<Card
title={t('dhcp_title')}
subtitle={t('dhcp_description')}
bodyType="card-body box-body--settings"
>
<div className="dhcp">
<Fragment>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{
interface_name,
...values,
}}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
/>
<hr />
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(interface_name)
}
disabled={
enabled || !interface_name || dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{!enabled && dhcp.check && (
<Fragment>
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
{this.getActiveDhcpMessage(t, dhcp.check)}
{this.getDhcpWarning(dhcp.check)}
</Fragment>
)}
</Fragment>
</div>
</div>
</Card>
}
</Card>
{dhcp.config.enabled && (
<Fragment>
<Card
title={t('dhcp_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Leases leases={dhcp.leases} />
</div>
</div>
</Card>
<Card
title={t('dhcp_static_leases')}
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={this.props.addStaticLease}
removeStaticLease={this.props.removeStaticLease}
toggleLeaseModal={this.props.toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => this.props.toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
</div>
</div>
</Card>
</Fragment>
)}
</Fragment>
)}
</Fragment>
);
}
@@ -166,7 +265,10 @@ Dhcp.propTypes = {
getDhcpStatus: PropTypes.func,
setDhcpConfig: PropTypes.func,
findActiveDhcp: PropTypes.func,
handleSubmit: PropTypes.func,
addStaticLease: PropTypes.func,
removeStaticLease: PropTypes.func,
toggleLeaseModal: PropTypes.func,
getDhcpInterfaces: PropTypes.func,
t: PropTypes.func,
};

View File

@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
const Form = (props) => {
const { handleSubmit, submitting, invalid } = props;
return (
<form onSubmit={handleSubmit}>
<div className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor="allowed_clients">
<Trans>access_allowed_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>access_allowed_desc</Trans>
</div>
<Field
id="allowed_clients"
name="allowed_clients"
component="textarea"
type="text"
className="form-control form-control--textarea"
/>
</div>
<div className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor="disallowed_clients">
<Trans>access_disallowed_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>access_disallowed_desc</Trans>
</div>
<Field
id="disallowed_clients"
name="disallowed_clients"
component="textarea"
type="text"
className="form-control form-control--textarea"
/>
</div>
<div className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor="blocked_hosts">
<Trans>access_blocked_title</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>access_blocked_desc</Trans>
</div>
<Field
id="blocked_hosts"
name="blocked_hosts"
component="textarea"
type="text"
className="form-control form-control--textarea"
/>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid}
>
<Trans>save_config</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
t: PropTypes.func,
};
export default flow([withNamespaces(), reduxForm({ form: 'accessForm' })])(Form);

View File

@@ -0,0 +1,43 @@
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 Access extends Component {
handleFormSubmit = (values) => {
this.props.setAccessList(values);
};
render() {
const { t, access } = this.props;
const {
processing,
processingSet,
...values
} = access;
return (
<Card
title={t('access_title')}
subtitle={t('access_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={values}
onSubmit={this.handleFormSubmit}
/>
</Card>
);
}
}
Access.propTypes = {
access: PropTypes.object.isRequired,
setAccessList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Access);

View File

@@ -4,17 +4,38 @@ import { Trans, withNamespaces } from 'react-i18next';
const Examples = props => (
<div className="list leading-loose">
<p>
<Trans
components={[
<a
href="https://kb.adguard.com/general/dns-providers"
target="_blank"
rel="noopener noreferrer"
key="0"
>
DNS providers
</a>,
]}
>
dns_providers
</Trans>
</p>
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
<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">
<a
href="https://en.wikipedia.org/wiki/DNS_over_TLS"
target="_blank"
rel="noopener noreferrer"
key="0"
>
DNS-over-TLS
</a>,
]}
@@ -28,7 +49,12 @@ const Examples = props => (
<span>
<Trans
components={[
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="0">
<a
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
target="_blank"
rel="noopener noreferrer"
key="0"
>
DNS-over-HTTPS
</a>,
]}
@@ -45,13 +71,28 @@ const Examples = props => (
<span>
<Trans
components={[
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
<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">
<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">
<a
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
target="_blank"
rel="noopener noreferrer"
key="2"
>
DNS-over-HTTPS
</a>,
]}
@@ -65,7 +106,12 @@ const Examples = props => (
<span>
<Trans
components={[
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains" target="_blank" rel="noopener noreferrer" key="0">
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer"
key="0"
>
Link
</a>,
]}

View File

@@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import classnames from 'classnames';
import { renderSelectField } from '../../../helpers/form';
import { renderSelectField } from '../../../../helpers/form';
import Examples from './Examples';
let Form = (props) => {
@@ -58,11 +58,11 @@ let Form = (props) => {
</div>
<div className="col-12">
<Examples />
<hr/>
<hr />
</div>
<div className="col-12">
<div className="form__group">
<label className="form__label" htmlFor="bootstrap_dns">
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
@@ -84,11 +84,13 @@ let Form = (props) => {
<button
type="button"
className={testButtonClass}
onClick={() => testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
})}
onClick={() =>
testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
})
}
disabled={!upstreamDns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
@@ -97,10 +99,7 @@ let Form = (props) => {
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| processingSetUpstream
|| processingTestUpstream
submitting || invalid || processingSetUpstream || processingTestUpstream
}
>
<Trans>apply_btn</Trans>
@@ -140,5 +139,7 @@ Form = connect((state) => {
export default flow([
withNamespaces(),
reduxForm({ form: 'upstreamForm' }),
reduxForm({
form: 'upstreamForm',
}),
])(Form);

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Form from './Form';
import Card from '../../ui/Card';
import Card from '../../../ui/Card';
class Upstream extends Component {
handleSubmit = (values) => {
@@ -12,7 +12,7 @@ class Upstream extends Component {
handleTest = (values) => {
this.props.testUpstream(values);
}
};
render() {
const {
@@ -26,8 +26,8 @@ class Upstream extends Component {
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
title={t('upstream_dns')}
subtitle={t('upstream_dns_hint')}
bodyType="card-body box-body--settings"
>
<div className="row">

View File

@@ -0,0 +1,60 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Upstream from './Upstream';
import Access from './Access';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Dns extends Component {
componentDidMount() {
this.props.getAccessList();
}
render() {
const {
t,
dashboard,
settings,
access,
setAccessList,
testUpstream,
setUpstream,
} = this.props;
return (
<Fragment>
<PageTitle title={t('dns_settings')} />
{(dashboard.processing || access.processing) && <Loading />}
{!dashboard.processing && !access.processing && (
<Fragment>
<Upstream
upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
allServers={dashboard.allServers}
processingTestUpstream={settings.processingTestUpstream}
processingSetUpstream={settings.processingSetUpstream}
setUpstream={setUpstream}
testUpstream={testUpstream}
/>
<Access access={access} setAccessList={setAccessList} />
</Fragment>
)}
</Fragment>
);
}
}
Dns.propTypes = {
dashboard: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired,
setAccessList: PropTypes.func.isRequired,
access: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Dns);

View File

@@ -66,14 +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);
const isSavingDisabled =
invalid ||
submitting ||
processingConfig ||
processingValidate ||
(isEnabled && (!privateKey || !certificateChain)) ||
(privateKey && !valid_key) ||
(certificateChain && !valid_cert) ||
(privateKey && certificateChain && !valid_pair);
return (
<form onSubmit={handleSubmit}>
@@ -91,7 +92,7 @@ let Form = (props) => {
<div className="form__desc">
<Trans>encryption_enable_desc</Trans>
</div>
<hr/>
<hr />
</div>
<div className="col-12">
<label className="form__label" htmlFor="server_name">
@@ -180,13 +181,20 @@ let Form = (props) => {
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label form__label--bold" htmlFor="certificate_chain">
<label
className="form__label form__label--bold"
htmlFor="certificate_chain"
>
<Trans>encryption_certificates</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans
values={{ link: 'letsencrypt.org' }}
components={[<a href="https://letsencrypt.org/" key="0">link</a>]}
components={[
<a href="https://letsencrypt.org/" key="0">
link
</a>,
]}
>
encryption_certificates_desc
</Trans>
@@ -202,49 +210,52 @@ let Form = (props) => {
disabled={!isEnabled}
/>
<div className="form__status">
{certificateChain &&
{certificateChain && (
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li className={valid_chain ? 'text-success' : 'text-danger'}>
{valid_chain ?
<li
className={valid_chain ? 'text-success' : 'text-danger'}
>
{valid_chain ? (
<Trans>encryption_chain_valid</Trans>
: <Trans>encryption_chain_invalid</Trans>
}
) : (
<Trans>encryption_chain_invalid</Trans>
)}
</li>
{valid_cert &&
{valid_cert && (
<Fragment>
{subject &&
{subject && (
<li>
<Trans>encryption_subject</Trans>:&nbsp;
{subject}
</li>
}
{issuer &&
)}
{issuer && (
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
}
{not_after && not_after !== EMPTY_DATE &&
)}
{not_after && not_after !== EMPTY_DATE && (
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
</li>
}
{dns_names &&
)}
{dns_names && (
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{dns_names}
</li>
}
)}
</Fragment>
}
)}
</ul>
</Fragment>
}
)}
</div>
</div>
</div>
@@ -261,40 +272,39 @@ 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}
/>
<div className="form__status">
{privateKey &&
{privateKey && (
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li className={valid_key ? 'text-success' : 'text-danger'}>
{valid_key ?
{valid_key ? (
<Trans values={{ type: key_type }}>
encryption_key_valid
</Trans>
: <Trans values={{ type: key_type }}>
) : (
<Trans values={{ type: key_type }}>
encryption_key_invalid
</Trans>
}
)}
</li>
</ul>
</Fragment>
}
)}
</div>
</div>
</div>
{warning_validation &&
{warning_validation && (
<div className="col-12">
<p className="text-danger">
{warning_validation}
</p>
<p className="text-danger">{warning_validation}</p>
</div>
}
)}
</div>
<div className="btn-list mt-2">

View File

@@ -6,10 +6,18 @@ import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
import Form from './Form';
import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Encryption extends Component {
componentDidMount() {
this.props.validateTlsConfig(this.props.encryption);
const { getTlsStatus, validateTlsConfig, encryption } = this.props;
getTlsStatus();
if (encryption.enabled) {
validateTlsConfig(encryption);
}
}
handleFormSubmit = (values) => {
@@ -34,7 +42,9 @@ class Encryption extends Component {
return (
<div className="encryption">
{encryption &&
<PageTitle title={t('encryption_settings')} />
{encryption.processing && <Loading />}
{!encryption.processing && (
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
@@ -56,7 +66,7 @@ class Encryption extends Component {
{...this.props.encryption}
/>
</Card>
}
)}
</div>
);
}

View File

@@ -63,6 +63,10 @@
font-weight: 700;
}
.form__label--with-desc {
margin-bottom: 0;
}
.form__status {
margin-top: 10px;
font-size: 14px;
@@ -76,3 +80,11 @@
.encryption__list li {
list-style: inside;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
}

View File

@@ -1,13 +1,12 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Upstream from './Upstream';
import Dhcp from './Dhcp';
import Encryption from './Encryption';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import './Settings.css';
class Settings extends Component {
@@ -36,9 +35,6 @@ class Settings extends Component {
componentDidMount() {
this.props.initSettings(this.settings);
this.props.getDhcpStatus();
this.props.getDhcpInterfaces();
this.props.getTlsStatus();
}
renderSettings = (settings) => {
@@ -46,58 +42,41 @@ class Settings extends Component {
return Object.keys(settings).map((key) => {
const setting = settings[key];
const { enabled } = setting;
return (<Checkbox
key={key}
{...settings[key]}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>);
return (
<Checkbox
key={key}
{...settings[key]}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>
);
});
}
return (
<div><Trans>no_settings</Trans></div>
<div>
<Trans>no_settings</Trans>
</div>
);
}
};
render() {
const { settings, dashboard, t } = this.props;
const { settings, t } = this.props;
return (
<Fragment>
<PageTitle title={ t('settings') } />
<PageTitle title={t('general_settings')} />
{settings.processing && <Loading />}
{!settings.processing &&
{!settings.processing && (
<div className="content">
<div className="row">
<div className="col-md-12">
<Card title={ t('general_settings') } bodyType="card-body box-body--settings">
<Card bodyType="card-body box-body--settings">
<div className="form">
{this.renderSettings(settings.settingsList)}
</div>
</Card>
<Upstream
upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
allServers={dashboard.allServers}
setUpstream={this.props.setUpstream}
testUpstream={this.props.testUpstream}
processingTestUpstream={settings.processingTestUpstream}
processingSetUpstream={settings.processingSetUpstream}
/>
<Encryption
encryption={this.props.encryption}
setTlsConfig={this.props.setTlsConfig}
validateTlsConfig={this.props.validateTlsConfig}
/>
<Dhcp
dhcp={this.props.dhcp}
toggleDhcp={this.props.toggleDhcp}
getDhcpStatus={this.props.getDhcpStatus}
findActiveDhcp={this.props.findActiveDhcp}
setDhcpConfig={this.props.setDhcpConfig}
/>
</div>
</div>
</div>
}
)}
</Fragment>
);
}
@@ -108,8 +87,6 @@ Settings.propTypes = {
settings: PropTypes.object,
settingsList: PropTypes.object,
toggleSetting: PropTypes.func,
handleUpstreamChange: PropTypes.func,
setUpstream: PropTypes.func,
t: PropTypes.func,
};

View File

@@ -2,7 +2,7 @@
position: fixed;
right: 24px;
bottom: 24px;
z-index: 103;
z-index: 105;
width: 345px;
}
@@ -32,6 +32,12 @@
overflow: hidden;
}
.toast__content a {
font-weight: 600;
color: #fff;
text-decoration: underline;
}
.toast__dismiss {
display: block;
flex: 0 0 auto;

View File

@@ -4,7 +4,7 @@ import { Trans, withNamespaces } from 'react-i18next';
class Toast extends Component {
componentDidMount() {
const timeout = this.props.type === 'error' ? 30000 : 5000;
const timeout = this.props.type === 'success' ? 5000 : 30000;
setTimeout(() => {
this.props.removeToast(this.props.id);
@@ -15,13 +15,25 @@ class Toast extends Component {
return false;
}
showMessage(t, type, message) {
if (type === 'notice') {
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
}
return <Trans>{message}</Trans>;
}
render() {
const {
type, id, t, message,
} = this.props;
return (
<div className={`toast toast--${this.props.type}`}>
<div className={`toast toast--${type}`}>
<p className="toast__content">
<Trans>{this.props.message}</Trans>
{this.showMessage(t, type, message)}
</p>
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
</button>
</div>
@@ -30,6 +42,7 @@ class Toast extends Component {
}
Toast.propTypes = {
t: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,

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

@@ -0,0 +1,8 @@
.dropdown-item.active,
.dropdown-item:active {
background-color: #66b574;
}
.dropdown-menu {
cursor: default;
}

View File

@@ -0,0 +1,89 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withNamespaces } from 'react-i18next';
import enhanceWithClickOutside from 'react-click-outside';
import './Dropdown.css';
class Dropdown extends Component {
state = {
isOpen: false,
};
toggleDropdown = () => {
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
};
hideDropdown = () => {
this.setState({ isOpen: false });
};
handleClickOutside = () => {
if (this.state.isOpen) {
this.hideDropdown();
}
};
render() {
const {
label,
controlClassName,
menuClassName,
baseClassName,
icon,
children,
} = this.props;
const { isOpen } = this.state;
const dropdownClass = classnames({
[baseClassName]: true,
show: isOpen,
});
const dropdownMenuClass = classnames({
[menuClassName]: true,
show: isOpen,
});
const ariaSettings = isOpen ? 'true' : 'false';
return (
<div className={dropdownClass}>
<a
className={controlClassName}
aria-expanded={ariaSettings}
onClick={this.toggleDropdown}
>
{icon && (
<svg className="nav-icon">
<use xlinkHref={`#${icon}`} />
</svg>
)}
{label}
</a>
<div className={dropdownMenuClass} onClick={this.hideDropdown}>
{children}
</div>
</div>
);
}
}
Dropdown.defaultProps = {
baseClassName: 'dropdown',
menuClassName: 'dropdown-menu dropdown-menu-arrow',
controlClassName: '',
};
Dropdown.propTypes = {
label: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
controlClassName: PropTypes.node.isRequired,
menuClassName: PropTypes.string.isRequired,
baseClassName: PropTypes.string.isRequired,
icon: PropTypes.string,
};
export default withNamespaces()(enhanceWithClickOutside(Dropdown));

View File

@@ -18,7 +18,7 @@ const EncryptionTopline = (props) => {
if (isExpired) {
return (
<Topline type="danger">
<Trans components={[<a href="#settings" key="0">link</a>]}>
<Trans components={[<a href="#encryption" key="0">link</a>]}>
topline_expired_certificate
</Trans>
</Topline>
@@ -26,7 +26,7 @@ const EncryptionTopline = (props) => {
} else if (isAboutExpire) {
return (
<Topline type="warning">
<Trans components={[<a href="#settings" key="0">link</a>]}>
<Trans components={[<a href="#encryption" key="0">link</a>]}>
topline_expiring_certificate
</Trans>
</Topline>

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,5 @@
.icons {
display: inline-block;
vertical-align: middle;
height: 100%;
}

View File

@@ -1,5 +1,7 @@
import React from 'react';
import './Icons.css';
const Icons = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
<symbol id="android" viewBox="0 0 14 16" fill="currentColor">
@@ -21,6 +23,38 @@ const Icons = () => (
<symbol id="router" viewBox="0 0 30 30" fill="currentColor">
<path d="M17.646 2.332a1 1 0 0 0-.697 1.719 6.984 6.984 0 0 1 0 9.898 1 1 0 1 0 1.414 1.414c3.507-3.506 3.507-9.22 0-12.726a1 1 0 0 0-.717-.305zm-12.662.654A1 1 0 0 0 4 4v14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H12V9a1 1 0 0 0-1.016-1.014A1 1 0 0 0 10 9v9H6V4a1 1 0 0 0-1.016-1.014zm9.834 2.176a1 1 0 0 0-.697 1.717 2.985 2.985 0 0 1 0 4.242 1 1 0 1 0 1.414 1.414 5.014 5.014 0 0 0 0-7.07 1 1 0 0 0-.717-.303zM5 21a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol>
<symbol id="edit" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</symbol>
<symbol id="delete" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/>
</symbol>
<symbol id="back" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/>
</symbol>
<symbol id="dashboard" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/>
</symbol>
<symbol id="filters" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/>
</symbol>
<symbol id="log" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/>
</symbol>
<symbol id="setup" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<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>
</symbol>
<symbol id="settings" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/>
</symbol>
</svg>
);

View File

@@ -5,7 +5,7 @@
overflow-x: hidden;
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
z-index: 104;
}
.ReactModal__Overlay--after-open {
@@ -38,3 +38,9 @@
border: none;
background-color: transparent;
}
@media (min-width: 576px) {
.modal-dialog--clients {
max-width: 650px;
}
}

View File

@@ -0,0 +1,40 @@
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 110;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
font-size: 28px;
font-weight: 600;
text-align: center;
background-color: rgba(255, 255, 255, 0.8);
}
.overlay--visible {
display: flex;
}
.overlay__loading {
width: 40px;
height: 40px;
margin-bottom: 20px;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E");
will-change: transform;
animation: clockwise 2s linear infinite;
}
@keyframes clockwise {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import classnames from 'classnames';
import './Overlay.css';
const UpdateOverlay = (props) => {
const overlayClass = classnames({
overlay: true,
'overlay--visible': props.processingUpdate,
});
return (
<div className={overlayClass}>
<div className="overlay__loading"></div>
<Trans>processing_update</Trans>
</div>
);
};
UpdateOverlay.propTypes = {
processingUpdate: PropTypes.bool,
};
export default withNamespaces()(UpdateOverlay);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
@@ -6,22 +6,37 @@ import Topline from './Topline';
const UpdateTopline = props => (
<Topline type="info">
<Trans
values={{ version: props.version }}
components={[
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}
>
update_announcement
</Trans>
<Fragment>
<Trans
values={{ version: props.version }}
components={[
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}
>
update_announcement
</Trans>
{props.canAutoUpdate &&
<button
type="button"
className="btn btn-sm btn-primary ml-3"
onClick={props.getUpdate}
disabled={props.processingUpdate}
>
<Trans>update_now</Trans>
</button>
}
</Fragment>
</Topline>
);
UpdateTopline.propTypes = {
version: PropTypes.string.isRequired,
version: PropTypes.string,
url: PropTypes.string.isRequired,
canAutoUpdate: PropTypes.bool,
getUpdate: PropTypes.func,
processingUpdate: PropTypes.bool,
};
export default withNamespaces()(UpdateTopline);

View File

@@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { getClients } from '../actions';
import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients';
import Clients from '../components/Settings/Clients';
const mapStateToProps = (state) => {
const { dashboard, clients } = state;
const props = {
dashboard,
clients,
};
return props;
};
const mapDispatchToProps = {
getClients,
addClient,
updateClient,
deleteClient,
toggleClientModal,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Clients);

View File

@@ -0,0 +1,36 @@
import { connect } from 'react-redux';
import {
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
toggleLeaseModal,
addStaticLease,
removeStaticLease,
} from '../actions';
import Dhcp from '../components/Settings/Dhcp';
const mapStateToProps = (state) => {
const { dhcp } = state;
const props = {
dhcp,
};
return props;
};
const mapDispatchToProps = {
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
toggleLeaseModal,
addStaticLease,
removeStaticLease,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Dhcp);

View File

@@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { handleUpstreamChange, setUpstream, testUpstream } from '../actions';
import { getAccessList, setAccessList } from '../actions/access';
import Dns from '../components/Settings/Dns';
const mapStateToProps = (state) => {
const { dashboard, settings, access } = state;
const props = {
dashboard,
settings,
access,
};
return props;
};
const mapDispatchToProps = {
handleUpstreamChange,
setUpstream,
testUpstream,
getAccessList,
setAccessList,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Dns);

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
import Encryption from '../components/Settings/Encryption';
const mapStateToProps = (state) => {
const { encryption } = state;
const props = {
encryption,
};
return props;
};
const mapDispatchToProps = {
getTlsStatus,
setTlsConfig,
validateTlsConfig,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Encryption);

View File

@@ -1,36 +1,11 @@
import { connect } from 'react-redux';
import {
initSettings,
toggleSetting,
handleUpstreamChange,
setUpstream,
testUpstream,
addErrorToast,
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
} from '../actions';
import {
getTlsStatus,
setTlsConfig,
validateTlsConfig,
} from '../actions/encryption';
import { initSettings, toggleSetting } from '../actions';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const {
settings,
dashboard,
dhcp,
encryption,
} = state;
const { settings } = state;
const props = {
settings,
dashboard,
dhcp,
encryption,
};
return props;
};
@@ -38,18 +13,6 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = {
initSettings,
toggleSetting,
handleUpstreamChange,
setUpstream,
testUpstream,
addErrorToast,
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
getTlsStatus,
setTlsConfig,
validateTlsConfig,
};
export default connect(

View File

@@ -1,5 +1,6 @@
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g;
export const STATS_NAMES = {
avg_processing_time: 'average_processing_time',
@@ -19,9 +20,12 @@ export const STATUS_COLORS = {
export const REPOSITORY = {
URL: 'https://github.com/AdguardTeam/AdGuardHome',
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
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',
@@ -155,3 +159,23 @@ 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',
};
export const MODAL_TYPE = {
ADD: 'add',
EDIT: 'edit',
};
export const CLIENT_ID = {
MAC: 'mac',
IP: 'ip',
};
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];

View File

@@ -1,7 +1,7 @@
import React, { Fragment } from 'react';
import { Trans } from 'react-i18next';
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
export const renderField = ({
input, id, className, placeholder, type, disabled, meta: { touched, error },
@@ -55,6 +55,13 @@ export const ipv4 = (value) => {
return false;
};
export const mac = (value) => {
if (value && !new RegExp(R_MAC).test(value)) {
return <Trans>form_error_mac_format</Trans>;
}
return false;
};
export const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;

View File

@@ -208,3 +208,20 @@ export const getClientName = (clients, ip) => {
const client = clients.find(item => ip === item.ip);
return (client && client.name) || '';
};
export const sortClients = (clients) => {
const compare = (a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA > nameB) {
return 1;
} else if (nameA < nameB) {
return -1;
}
return 0;
};
return clients.sort(compare);
};

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

@@ -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,199 @@ const renderInterfaces = (interfaces => (
})
));
let Settings = (props) => {
const {
handleSubmit,
webIp,
webPort,
dnsIp,
dnsPort,
interfaces,
invalid,
webWarning,
dnsWarning,
} = props;
class Settings extends Component {
componentDidMount() {
const {
webIp, webPort, dnsIp, dnsPort,
} = this.props;
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: {
ip: webIp,
port: webPort,
},
dns: {
ip: dnsIp,
port: dnsPort,
},
});
}
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 +262,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 +269,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 +290,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

@@ -0,0 +1,44 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/access';
const access = handleActions(
{
[actions.getAccessListRequest]: state => ({ ...state, processing: true }),
[actions.getAccessListFailure]: state => ({ ...state, processing: false }),
[actions.getAccessListSuccess]: (state, { payload }) => {
const {
allowed_clients,
disallowed_clients,
blocked_hosts,
} = payload;
const newState = {
...state,
allowed_clients: (allowed_clients && allowed_clients.join('\n')) || '',
disallowed_clients: (disallowed_clients && disallowed_clients.join('\n')) || '',
blocked_hosts: (blocked_hosts && blocked_hosts.join('\n')) || '',
processing: false,
};
return newState;
},
[actions.setAccessListRequest]: state => ({ ...state, processingSet: true }),
[actions.setAccessListFailure]: state => ({ ...state, processingSet: false }),
[actions.setAccessListSuccess]: (state) => {
const newState = {
...state,
processingSet: false,
};
return newState;
},
},
{
processing: true,
processingSet: false,
allowed_clients: '',
disallowed_clients: '',
blocked_hosts: '',
},
);
export default access;

View File

@@ -0,0 +1,63 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/clients';
const clients = handleActions({
[actions.addClientRequest]: state => ({ ...state, processingAdding: true }),
[actions.addClientFailure]: state => ({ ...state, processingAdding: false }),
[actions.addClientSuccess]: (state) => {
const newState = {
...state,
processingAdding: false,
};
return newState;
},
[actions.deleteClientRequest]: state => ({ ...state, processingDeleting: true }),
[actions.deleteClientFailure]: state => ({ ...state, processingDeleting: false }),
[actions.deleteClientSuccess]: (state) => {
const newState = {
...state,
processingDeleting: false,
};
return newState;
},
[actions.updateClientRequest]: state => ({ ...state, processingUpdating: true }),
[actions.updateClientFailure]: state => ({ ...state, processingUpdating: false }),
[actions.updateClientSuccess]: (state) => {
const newState = {
...state,
processingUpdating: false,
};
return newState;
},
[actions.toggleClientModal]: (state, { payload }) => {
if (payload) {
const newState = {
...state,
modalType: payload.type || '',
modalClientName: payload.name || '',
isModalOpen: !state.isModalOpen,
};
return newState;
}
const newState = {
...state,
isModalOpen: !state.isModalOpen,
};
return newState;
},
}, {
processing: true,
processingAdding: false,
processingDeleting: false,
processingUpdating: false,
isModalOpen: false,
modalClientName: '',
modalType: '',
});
export default clients;

View File

@@ -7,6 +7,8 @@ import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
import toasts from './toasts';
import encryption from './encryption';
import clients from './clients';
import access from './access';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@@ -122,16 +124,18 @@ const dashboard = handleActions({
[actions.getVersionSuccess]: (state, { payload }) => {
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
if (versionCompare(currentVersion, payload.version) === -1) {
if (payload && versionCompare(currentVersion, payload.new_version) === -1) {
const {
version,
announcement_url: announcementUrl,
new_version: newVersion,
can_autoupdate: canAutoUpdate,
} = payload;
const newState = {
...state,
version,
announcementUrl,
newVersion,
canAutoUpdate,
isUpdateAvailable: true,
};
return newState;
@@ -140,6 +144,13 @@ const dashboard = handleActions({
return state;
},
[actions.getUpdateRequest]: state => ({ ...state, processingUpdate: true }),
[actions.getUpdateFailure]: state => ({ ...state, processingUpdate: false }),
[actions.getUpdateSuccess]: (state) => {
const newState = { ...state, processingUpdate: false };
return newState;
},
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
[actions.getFilteringSuccess]: (state, { payload }) => {
@@ -173,7 +184,8 @@ const dashboard = handleActions({
[actions.getClientsSuccess]: (state, { payload }) => {
const newState = {
...state,
clients: payload,
clients: payload.clients,
autoClients: payload.autoClients,
processingClients: false,
};
return newState;
@@ -187,6 +199,7 @@ const dashboard = handleActions({
processingVersion: true,
processingFiltering: true,
processingClients: true,
processingUpdate: false,
upstreamDns: '',
bootstrapDns: '',
allServers: false,
@@ -197,6 +210,8 @@ const dashboard = handleActions({
dnsAddresses: [],
dnsVersion: '',
clients: [],
autoClients: [],
topStats: [],
});
const queryLogs = handleActions({
@@ -271,11 +286,18 @@ const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
const {
static_leases: staticLeases,
...values
} = payload;
const newState = {
...state,
...payload,
staticLeases,
processing: false,
...values,
};
return newState;
},
@@ -292,18 +314,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;
},
@@ -315,17 +350,62 @@ const dhcp = handleActions({
const newState = { ...state, config: newConfig, processingConfig: false };
return newState;
},
[actions.toggleLeaseModal]: (state) => {
const newState = {
...state,
isModalOpen: !state.isModalOpen,
};
return newState;
},
[actions.addStaticLeaseRequest]: state => ({ ...state, processingAdding: true }),
[actions.addStaticLeaseFailure]: state => ({ ...state, processingAdding: false }),
[actions.addStaticLeaseSuccess]: (state, { payload }) => {
const {
ip, mac, hostname,
} = payload;
const newLease = {
ip,
mac,
hostname: hostname || '',
};
const leases = [...state.staticLeases, newLease];
const newState = {
...state,
staticLeases: leases,
processingAdding: false,
};
return newState;
},
[actions.removeStaticLeaseRequest]: state => ({ ...state, processingDeleting: true }),
[actions.removeStaticLeaseFailure]: state => ({ ...state, processingDeleting: false }),
[actions.removeStaticLeaseSuccess]: (state, { payload }) => {
const leaseToRemove = payload.ip;
const leases = state.staticLeases.filter(item => item.ip !== leaseToRemove);
const newState = {
...state,
staticLeases: leases,
processingDeleting: false,
};
return newState;
},
}, {
processing: true,
processingStatus: false,
processingInterfaces: false,
processingDhcp: false,
processingConfig: false,
processingAdding: false,
processingDeleting: false,
config: {
enabled: false,
},
active: null,
check: null,
leases: [],
staticLeases: [],
isModalOpen: false,
});
export default combineReducers({
@@ -336,6 +416,8 @@ export default combineReducers({
toasts,
dhcp,
encryption,
clients,
access,
loadingBar: loadingBarReducer,
form: formReducer,
});

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

@@ -1,7 +1,7 @@
import { handleActions } from 'redux-actions';
import nanoid from 'nanoid';
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
import { addErrorToast, addSuccessToast, addNoticeToast, removeToast } from '../actions';
const toasts = handleActions({
[addErrorToast]: (state, { payload }) => {
@@ -24,6 +24,16 @@ const toasts = handleActions({
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[addNoticeToast]: (state, { payload }) => {
const noticeToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'notice',
};
const newState = { ...state, notices: [...state.notices, noticeToast] };
return newState;
},
[removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };

View File

@@ -12,7 +12,7 @@ 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 FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.png');
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');

View File

@@ -2,32 +2,264 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"runtime"
"strings"
"sync"
"github.com/AdguardTeam/golibs/log"
)
// Client information
type Client struct {
IP string
Name string
//Source source // Hosts file / User settings / DHCP
IP string
MAC string
Name string
UseOwnSettings bool // false: use global settings
FilteringEnabled bool
SafeSearchEnabled bool
SafeBrowsingEnabled bool
ParentalEnabled bool
}
type clientJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
IP string `json:"ip"`
MAC string `json:"mac"`
Name string `json:"name"`
UseGlobalSettings bool `json:"use_global_settings"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
}
var clients []Client
var clientsFilled bool
type clientSource uint
const (
ClientSourceHostsFile clientSource = 0 // from /etc/hosts
ClientSourceRDNS clientSource = 1 // from rDNS
)
// ClientHost information
type ClientHost struct {
Host string
Source clientSource
}
type clientsContainer struct {
list map[string]*Client
ipIndex map[string]*Client
ipHost map[string]ClientHost // IP -> Hostname
lock sync.Mutex
}
var clients clientsContainer
// Initialize clients container
func clientsInit() {
if clients.list != nil {
log.Fatal("clients.list != nil")
}
clients.list = make(map[string]*Client)
clients.ipIndex = make(map[string]*Client)
clients.ipHost = make(map[string]ClientHost)
clientsAddFromHostsFile()
}
func clientsGetList() map[string]*Client {
return clients.list
}
func clientExists(ip string) bool {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.ipIndex[ip]
if ok {
return true
}
_, ok = clients.ipHost[ip]
return ok
}
// Search for a client by IP
func clientFind(ip string) (Client, bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.ipIndex[ip]
if ok {
return *c, true
}
for _, c = range clients.list {
if len(c.MAC) != 0 {
mac, err := net.ParseMAC(c.MAC)
if err != nil {
continue
}
ipAddr := dhcpServer.FindIPbyMAC(mac)
if ipAddr == nil {
continue
}
if ip == ipAddr.String() {
return *c, true
}
}
}
return Client{}, false
}
// Check if Client object's fields are correct
func clientCheck(c *Client) error {
if len(c.Name) == 0 {
return fmt.Errorf("Invalid Name")
}
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
(len(c.IP) != 0 && len(c.MAC) != 0) {
return fmt.Errorf("IP or MAC required")
}
if len(c.IP) != 0 {
ip := net.ParseIP(c.IP)
if ip == nil {
return fmt.Errorf("Invalid IP")
}
c.IP = ip.String()
} else {
_, err := net.ParseMAC(c.MAC)
if err != nil {
return fmt.Errorf("Invalid MAC: %s", err)
}
}
return nil
}
// Add a new client object
// Return true: success; false: client exists.
func clientAdd(c Client) (bool, error) {
e := clientCheck(&c)
if e != nil {
return false, e
}
clients.lock.Lock()
defer clients.lock.Unlock()
// check Name index
_, ok := clients.list[c.Name]
if ok {
return false, nil
}
// check IP index
if len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
if ok {
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
}
}
clients.list[c.Name] = &c
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
}
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
return true, nil
}
// Remove a client
func clientDel(name string) bool {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.list[name]
if !ok {
return false
}
delete(clients.list, name)
delete(clients.ipIndex, c.IP)
return true
}
// Update a client
func clientUpdate(name string, c Client) error {
err := clientCheck(&c)
if err != nil {
return err
}
clients.lock.Lock()
defer clients.lock.Unlock()
old, ok := clients.list[name]
if !ok {
return fmt.Errorf("Client not found")
}
// check Name index
if old.Name != c.Name {
_, ok = clients.list[c.Name]
if ok {
return fmt.Errorf("Client already exists")
}
}
// check IP index
if old.IP != c.IP && len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
if ok {
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
}
}
// update Name index
if old.Name != c.Name {
delete(clients.list, old.Name)
}
clients.list[c.Name] = &c
// update IP index
if old.IP != c.IP {
delete(clients.ipIndex, old.IP)
}
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
}
return nil
}
func clientAddHost(ip, host string, source clientSource) (bool, error) {
clients.lock.Lock()
defer clients.lock.Unlock()
// check index
_, ok := clients.ipHost[ip]
if ok {
return false, nil
}
clients.ipHost[ip] = ClientHost{
Host: host,
Source: source,
}
log.Tracef("'%s': '%s' -> [%d]", host, ip, len(clients.ipHost))
return true, nil
}
// Parse system 'hosts' file and fill clients array
func fillClientInfo() {
func clientsAddFromHostsFile() {
hostsFn := "/etc/hosts"
if runtime.GOOS == "windows" {
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
@@ -40,6 +272,7 @@ func fillClientInfo() {
}
lines := strings.Split(string(d), "\n")
n := 0
for _, ln := range lines {
ln = strings.TrimSpace(ln)
if len(ln) == 0 || ln[0] == '#' {
@@ -51,33 +284,71 @@ func fillClientInfo() {
continue
}
var c Client
c.IP = fields[0]
c.Name = fields[1]
clients = append(clients, c)
log.Tracef("%s -> %s", c.IP, c.Name)
ok, e := clientAddHost(fields[0], fields[1], ClientSourceHostsFile)
if e != nil {
log.Tracef("%s", e)
}
if ok {
n++
}
}
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
clientsFilled = true
log.Info("Added %d client aliases from %s", n, hostsFn)
}
type clientHostJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
Source string `json:"source"`
}
type clientListJSON struct {
Clients []clientJSON `json:"clients"`
AutoClients []clientHostJSON `json:"auto_clients"`
}
// 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 := clientListJSON{}
data := []clientJSON{}
for _, c := range clients {
clients.lock.Lock()
for _, c := range clients.list {
cj := clientJSON{
IP: c.IP,
Name: c.Name,
IP: c.IP,
MAC: c.MAC,
Name: c.Name,
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled,
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
}
data = append(data, cj)
if len(c.MAC) != 0 {
hwAddr, _ := net.ParseMAC(c.MAC)
ipAddr := dhcpServer.FindIPbyMAC(hwAddr)
if ipAddr != nil {
cj.IP = ipAddr.String()
}
}
data.Clients = append(data.Clients, cj)
}
for ip, ch := range clients.ipHost {
cj := clientHostJSON{
IP: ip,
Name: ch.Host,
}
cj.Source = "etc/hosts"
if ch.Source == ClientSourceRDNS {
cj.Source = "rDNS"
}
data.AutoClients = append(data.AutoClients, cj)
}
clients.lock.Unlock()
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
@@ -86,7 +357,126 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) {
}
}
// Convert JSON object to Client object
func jsonToClient(cj clientJSON) (*Client, error) {
c := Client{
IP: cj.IP,
MAC: cj.MAC,
Name: cj.Name,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
}
return &c, nil
}
// Add a new client
func handleAddClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
c, err := jsonToClient(cj)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
ok, err := clientAdd(*c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
if !ok {
httpError(w, http.StatusBadRequest, "Client already exists")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// Remove client
func handleDelClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil || len(cj.Name) == 0 {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if !clientDel(cj.Name) {
httpError(w, http.StatusBadRequest, "Client not found")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
type clientUpdateJSON struct {
Name string `json:"name"`
Data clientJSON `json:"data"`
}
// Update client's properties
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
var dj clientUpdateJSON
err = json.Unmarshal(body, &dj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if len(dj.Name) == 0 {
httpError(w, http.StatusBadRequest, "Invalid request")
return
}
c, err := jsonToClient(dj.Data)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = clientUpdate(dj.Name, *c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// RegisterClientsHandlers registers HTTP handlers
func RegisterClientsHandlers() {
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
http.HandleFunc("/control/clients/add", postInstall(optionalAuth(ensurePOST(handleAddClient))))
http.HandleFunc("/control/clients/delete", postInstall(optionalAuth(ensurePOST(handleDelClient))))
http.HandleFunc("/control/clients/update", postInstall(optionalAuth(ensurePOST(handleUpdateClient))))
}

122
clients_test.go Normal file
View File

@@ -0,0 +1,122 @@
package main
import "testing"
func TestClients(t *testing.T) {
var c Client
var e error
var b bool
clientsInit()
// add
c = Client{
IP: "1.1.1.1",
Name: "client1",
}
b, e = clientAdd(c)
if !b || e != nil {
t.Fatalf("clientAdd #1")
}
// add #2
c = Client{
IP: "2.2.2.2",
Name: "client2",
}
b, e = clientAdd(c)
if !b || e != nil {
t.Fatalf("clientAdd #2")
}
// failed add - name in use
c = Client{
IP: "1.2.3.5",
Name: "client1",
}
b, _ = clientAdd(c)
if b {
t.Fatalf("clientAdd - name in use")
}
// failed add - ip in use
c = Client{
IP: "2.2.2.2",
Name: "client3",
}
b, e = clientAdd(c)
if b || e == nil {
t.Fatalf("clientAdd - ip in use")
}
// get
if clientExists("1.2.3.4") {
t.Fatalf("clientExists")
}
if !clientExists("1.1.1.1") {
t.Fatalf("clientExists #1")
}
if !clientExists("2.2.2.2") {
t.Fatalf("clientExists #2")
}
// failed update - no such name
c.IP = "1.2.3.0"
c.Name = "client3"
if clientUpdate("client3", c) == nil {
t.Fatalf("clientUpdate")
}
// failed update - name in use
c.IP = "1.2.3.0"
c.Name = "client2"
if clientUpdate("client1", c) == nil {
t.Fatalf("clientUpdate - name in use")
}
// failed update - ip in use
c.IP = "2.2.2.2"
c.Name = "client1"
if clientUpdate("client1", c) == nil {
t.Fatalf("clientUpdate - ip in use")
}
// update
c.IP = "1.1.1.2"
c.Name = "client1"
if clientUpdate("client1", c) != nil {
t.Fatalf("clientUpdate")
}
// get after update
if clientExists("1.1.1.1") || !clientExists("1.1.1.2") {
t.Fatalf("clientExists - get after update")
}
// failed remove - no such name
if clientDel("client3") {
t.Fatalf("clientDel - no such name")
}
// remove
if !clientDel("client1") || clientExists("1.1.1.2") {
t.Fatalf("clientDel")
}
// add host client
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
if !b || e != nil {
t.Fatalf("clientAddHost")
}
// failed add - ip exists
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
if b || e != nil {
t.Fatalf("clientAddHost - ip exists")
}
// get
if !clientExists("1.1.1.1") {
t.Fatalf("clientAddHost")
}
}

108
config.go
View File

@@ -27,24 +27,47 @@ type logSettings struct {
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
}
type clientObject struct {
Name string `yaml:"name"`
IP string `yaml:"ip"`
MAC string `yaml:"mac"`
UseGlobalSettings bool `yaml:"use_global_settings"`
FilteringEnabled bool `yaml:"filtering_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
}
// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
type configuration struct {
// Raw file data to avoid re-reading of configuration file
// It's reset after config is parsed
fileData []byte
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
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
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
disableUpdate bool // If set, don't check for updates
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)
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
DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
// Note: this array is filled only before file read/write and then it's cleared
Clients []clientObject `yaml:"clients"`
logSettings `yaml:",inline"`
sync.RWMutex `yaml:"-"`
@@ -111,9 +134,10 @@ var config = configuration{
BindHost: "0.0.0.0",
Port: 53,
FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists
BlockedResponseTTL: 10, // in seconds
ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists
BlockingMode: "nxdomain", // mode how to answer filtered requests
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
Ratelimit: 20,
RefuseAny: true,
@@ -134,6 +158,10 @@ var config = configuration{
{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: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400,
ICMPTimeout: 1000,
},
SchemaVersion: currentSchemaVersion,
}
@@ -167,7 +195,7 @@ func (c *configuration) getConfigFilename() string {
func getLogSettings() logSettings {
l := logSettings{}
yamlFile, err := readConfigFile()
if err != nil || yamlFile == nil {
if err != nil {
return l
}
err = yaml.Unmarshal(yamlFile, &l)
@@ -183,19 +211,33 @@ func parseConfig() error {
log.Debug("Reading config file: %s", configFile)
yamlFile, err := readConfigFile()
if err != nil {
log.Error("Couldn't read config file: %s", err)
return err
}
if yamlFile == nil {
log.Error("YAML file doesn't exist, skipping it")
return nil
}
config.fileData = nil
err = yaml.Unmarshal(yamlFile, &config)
if err != nil {
log.Error("Couldn't parse config file: %s", err)
return err
}
for _, cy := range config.Clients {
cli := Client{
Name: cy.Name,
IP: cy.IP,
MAC: cy.MAC,
UseOwnSettings: !cy.UseGlobalSettings,
FilteringEnabled: cy.FilteringEnabled,
ParentalEnabled: cy.ParentalEnabled,
SafeSearchEnabled: cy.SafeSearchEnabled,
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
}
_, err = clientAdd(cli)
if err != nil {
log.Tracef("clientAdd: %s", err)
}
}
config.Clients = nil
// Deduplicate filters
deduplicateFilters()
@@ -206,25 +248,47 @@ func parseConfig() error {
// readConfigFile reads config file contents if it exists
func readConfigFile() ([]byte, error) {
configFile := config.getConfigFilename()
if _, err := os.Stat(configFile); os.IsNotExist(err) {
// do nothing, file doesn't exist
return nil, nil
if len(config.fileData) != 0 {
return config.fileData, nil
}
return ioutil.ReadFile(configFile)
configFile := config.getConfigFilename()
d, err := ioutil.ReadFile(configFile)
if err != nil {
log.Error("Couldn't read config file %s: %s", configFile, err)
return nil, err
}
return d, nil
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
func (c *configuration) write() error {
c.Lock()
defer c.Unlock()
if config.firstRun {
log.Debug("Silently refusing to write config because first run and not configured yet")
return nil
clientsList := clientsGetList()
for _, cli := range clientsList {
ip := cli.IP
if len(cli.MAC) != 0 {
ip = ""
}
cy := clientObject{
Name: cli.Name,
IP: ip,
MAC: cli.MAC,
UseGlobalSettings: !cli.UseOwnSettings,
FilteringEnabled: cli.FilteringEnabled,
ParentalEnabled: cli.ParentalEnabled,
SafeSearchEnabled: cli.SafeSearchEnabled,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
}
config.Clients = append(config.Clients, cy)
}
configFile := config.getConfigFilename()
log.Debug("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
config.Clients = nil
if err != nil {
log.Error("Couldn't generate YAML file: %s", err)
return err

View File

@@ -2,7 +2,6 @@ package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -19,6 +18,7 @@ import (
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils"
"github.com/NYTimes/gziphandler"
"github.com/miekg/dns"
govalidator "gopkg.in/asaskevich/govalidator.v4"
)
@@ -31,11 +31,13 @@ var versionCheckLastTime time.Time
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
const versionCheckURL = "https://adguardteam.github.io/AdGuardHome/version.json"
const versionCheckPeriod = time.Hour * 8
var transport = &http.Transport{
DialContext: customDialContext,
}
var client = &http.Client{
Timeout: time.Second * 30,
Timeout: time.Minute * 5,
Transport: transport,
}
var controlLock sync.Mutex
@@ -552,42 +554,6 @@ func checkDNS(input string, bootstrap []string) error {
return nil
}
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
now := time.Now()
if now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0 {
// return cached copy
w.Header().Set("Content-Type", "application/json")
w.Write(versionCheckJSON)
return
}
resp, err := client.Get(versionCheckURL)
if err != nil {
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
return
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
// read the body entirely
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(body)
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
}
versionCheckLastTime = now
versionCheckJSON = body
}
// ---------
// filtering
// ---------
@@ -707,19 +673,18 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
parameters, err := parseParametersFromBody(r.Body)
type request struct {
URL string `json:"url"`
}
req := request{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
return
}
url, ok := parameters["url"]
if !ok {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(url); !valid {
if valid := govalidator.IsRequestURL(req.URL); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
@@ -728,7 +693,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
config.Lock()
newFilters := config.Filters[:0]
for _, filter := range config.Filters {
if filter.URL != url {
if filter.URL != req.URL {
newFilters = append(newFilters, filter)
} else {
// Remove the filter file
@@ -866,7 +831,7 @@ func handleParentalEnable(w http.ResponseWriter, r *http.Request) {
sensitivity, ok := parameters["sensitivity"]
if !ok {
http.Error(w, "URL parameter was not specified", 400)
http.Error(w, "Sensitivity parameter was not specified", 400)
return
}
@@ -964,112 +929,6 @@ func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
}
}
type ipport struct {
IP string `json:"ip,omitempty"`
Port int `json:"port"`
Warning string `json:"warning"`
}
type firstRunData struct {
Web ipport `json:"web"`
DNS ipport `json:"dns"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Interfaces map[string]interface{} `json:"interfaces"`
}
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data := firstRunData{}
// find out if port 80 is available -- if not, fall back to 3000
if checkPortAvailable("", 80) == nil {
data.Web.Port = 80
} else {
data.Web.Port = 3000
}
// find out if port 53 is available -- if not, show a big warning
data.DNS.Port = 53
if checkPacketPortAvailable("", 53) != nil {
data.DNS.Warning = "Port 53 is not available for binding -- this will make DNS clients unable to contact AdGuard Home."
}
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
}
}
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
newSettings := firstRunData{}
err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse new config json: %s", err)
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, "Impossible to listen on IP:port %s due to %s", net.JoinHostPort(newSettings.DNS.IP, strconv.Itoa(newSettings.DNS.Port)), err)
return
}
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
if config.DNS.Port != 0 {
err = startDNSServer()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
return
}
}
httpUpdateConfigReloadDNSReturnOK(w, r)
// 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())
}()
}
}
// --------------
// DNS-over-HTTPS
// --------------
@@ -1091,16 +950,11 @@ func handleDOH(w http.ResponseWriter, r *http.Request) {
// ------------------------
// registration of handlers
// ------------------------
func registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
}
func registerControlHandlers() {
http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus))))
http.HandleFunc("/control/enable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionEnable))))
http.HandleFunc("/control/disable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionDisable))))
http.HandleFunc("/control/querylog", postInstall(optionalAuth(ensureGET(handleQueryLog))))
http.Handle("/control/querylog", postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureGETHandler(handleQueryLog)))))
http.HandleFunc("/control/querylog_enable", postInstall(optionalAuth(ensurePOST(handleQueryLogEnable))))
http.HandleFunc("/control/querylog_disable", postInstall(optionalAuth(ensurePOST(handleQueryLogDisable))))
http.HandleFunc("/control/set_upstreams_config", postInstall(optionalAuth(ensurePOST(handleSetUpstreamConfig))))
@@ -1112,15 +966,16 @@ func registerControlHandlers() {
http.HandleFunc("/control/stats_history", postInstall(optionalAuth(ensureGET(handleStatsHistory))))
http.HandleFunc("/control/stats_reset", postInstall(optionalAuth(ensurePOST(handleStatsReset))))
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
http.HandleFunc("/control/update", postInstall(optionalAuth(ensurePOST(handleUpdate))))
http.HandleFunc("/control/filtering/enable", postInstall(optionalAuth(ensurePOST(handleFilteringEnable))))
http.HandleFunc("/control/filtering/disable", postInstall(optionalAuth(ensurePOST(handleFilteringDisable))))
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePUT(handleFilteringAddURL))))
http.HandleFunc("/control/filtering/remove_url", postInstall(optionalAuth(ensureDELETE(handleFilteringRemoveURL))))
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePOST(handleFilteringAddURL))))
http.HandleFunc("/control/filtering/remove_url", postInstall(optionalAuth(ensurePOST(handleFilteringRemoveURL))))
http.HandleFunc("/control/filtering/enable_url", postInstall(optionalAuth(ensurePOST(handleFilteringEnableURL))))
http.HandleFunc("/control/filtering/disable_url", postInstall(optionalAuth(ensurePOST(handleFilteringDisableURL))))
http.HandleFunc("/control/filtering/refresh", postInstall(optionalAuth(ensurePOST(handleFilteringRefresh))))
http.HandleFunc("/control/filtering/status", postInstall(optionalAuth(ensureGET(handleFilteringStatus))))
http.HandleFunc("/control/filtering/set_rules", postInstall(optionalAuth(ensurePUT(handleFilteringSetRules))))
http.HandleFunc("/control/filtering/set_rules", postInstall(optionalAuth(ensurePOST(handleFilteringSetRules))))
http.HandleFunc("/control/safebrowsing/enable", postInstall(optionalAuth(ensurePOST(handleSafeBrowsingEnable))))
http.HandleFunc("/control/safebrowsing/disable", postInstall(optionalAuth(ensurePOST(handleSafeBrowsingDisable))))
http.HandleFunc("/control/safebrowsing/status", postInstall(optionalAuth(ensureGET(handleSafeBrowsingStatus))))
@@ -1134,6 +989,11 @@ func registerControlHandlers() {
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
http.HandleFunc("/control/dhcp/add_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPAddStaticLease))))
http.HandleFunc("/control/dhcp/remove_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPRemoveStaticLease))))
http.HandleFunc("/control/access/list", postInstall(optionalAuth(ensureGET(handleAccessList))))
http.HandleFunc("/control/access/set", postInstall(optionalAuth(ensurePOST(handleAccessSet))))
RegisterTLSHandlers()
RegisterClientsHandlers()

87
control_access.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"encoding/json"
"net"
"net/http"
"github.com/AdguardTeam/golibs/log"
)
type accessListJSON struct {
AllowedClients []string `json:"allowed_clients"`
DisallowedClients []string `json:"disallowed_clients"`
BlockedHosts []string `json:"blocked_hosts"`
}
func handleAccessList(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
controlLock.Lock()
j := accessListJSON{
AllowedClients: config.DNS.AllowedClients,
DisallowedClients: config.DNS.DisallowedClients,
BlockedHosts: config.DNS.BlockedHosts,
}
controlLock.Unlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(j)
if err != nil {
httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
return
}
}
func checkIPCIDRArray(src []string) error {
for _, s := range src {
ip := net.ParseIP(s)
if ip != nil {
continue
}
_, _, err := net.ParseCIDR(s)
if err != nil {
return err
}
}
return nil
}
func handleAccessSet(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
j := accessListJSON{}
err := json.NewDecoder(r.Body).Decode(&j)
if err != nil {
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
err = checkIPCIDRArray(j.AllowedClients)
if err == nil {
err = checkIPCIDRArray(j.DisallowedClients)
}
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
config.Lock()
config.DNS.AllowedClients = j.AllowedClients
config.DNS.DisallowedClients = j.DisallowedClients
config.DNS.BlockedHosts = j.BlockedHosts
config.Unlock()
log.Tracef("Update access lists: %d, %d, %d",
len(j.AllowedClients), len(j.DisallowedClients), len(j.BlockedHosts))
err = writeAllConfigsAndReloadDNS()
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
returnOK(w)
}

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)))
}

511
control_update.go Normal file
View File

@@ -0,0 +1,511 @@
package main
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/AdguardTeam/golibs/log"
)
// Convert version.json data to our JSON response
func getVersionResp(data []byte) []byte {
versionJSON := make(map[string]interface{})
err := json.Unmarshal(data, &versionJSON)
if err != nil {
log.Error("version.json: %s", err)
return []byte{}
}
ret := make(map[string]interface{})
ret["can_autoupdate"] = false
var ok1, ok2, ok3 bool
ret["new_version"], ok1 = versionJSON["version"].(string)
ret["announcement"], ok2 = versionJSON["announcement"].(string)
ret["announcement_url"], ok3 = versionJSON["announcement_url"].(string)
selfUpdateMinVersion, ok4 := versionJSON["selfupdate_min_version"].(string)
if !ok1 || !ok2 || !ok3 || !ok4 {
log.Error("version.json: invalid data")
return []byte{}
}
_, ok := versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)]
if ok && ret["new_version"] != VersionString && VersionString >= selfUpdateMinVersion {
ret["can_autoupdate"] = true
}
d, _ := json.Marshal(ret)
return d
}
// Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
if config.disableUpdate {
log.Tracef("New app version check is disabled by user")
return
}
now := time.Now()
controlLock.Lock()
cached := now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0
data := versionCheckJSON
controlLock.Unlock()
if cached {
// return cached copy
w.Header().Set("Content-Type", "application/json")
w.Write(getVersionResp(data))
return
}
resp, err := client.Get(versionCheckURL)
if err != nil {
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
return
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
// read the body entirely
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
return
}
controlLock.Lock()
versionCheckLastTime = now
versionCheckJSON = body
controlLock.Unlock()
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(getVersionResp(body))
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
}
}
// Copy file on disk
func copyFile(src, dst string) error {
d, e := ioutil.ReadFile(src)
if e != nil {
return e
}
e = ioutil.WriteFile(dst, d, 0644)
if e != nil {
return e
}
return nil
}
type updateInfo struct {
pkgURL string // URL for the new package
pkgName string // Full path to package file
newVer string // New version string
updateDir string // Full path to the directory containing unpacked files from the new package
backupDir string // Full path to backup directory
configName string // Full path to the current configuration file
updateConfigName string // Full path to the configuration file to check by the new binary
curBinName string // Full path to the current executable file
bkpBinName string // Full path to the current executable file in backup directory
newBinName string // Full path to the new executable file
}
// Fill in updateInfo object
func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
var u updateInfo
workDir := config.ourWorkingDir
versionJSON := make(map[string]interface{})
err := json.Unmarshal(jsonData, &versionJSON)
if err != nil {
return nil, fmt.Errorf("JSON parse: %s", err)
}
u.pkgURL = versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)].(string)
u.newVer = versionJSON["version"].(string)
if len(u.pkgURL) == 0 || len(u.newVer) == 0 {
return nil, fmt.Errorf("Invalid JSON")
}
if u.newVer == VersionString {
return nil, fmt.Errorf("No need to update")
}
u.updateDir = filepath.Join(workDir, fmt.Sprintf("agh-update-%s", u.newVer))
u.backupDir = filepath.Join(workDir, fmt.Sprintf("agh-backup-%s", VersionString))
_, pkgFileName := filepath.Split(u.pkgURL)
if len(pkgFileName) == 0 {
return nil, fmt.Errorf("Invalid JSON")
}
u.pkgName = filepath.Join(u.updateDir, pkgFileName)
u.configName = config.getConfigFilename()
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome", "AdGuardHome.yaml")
if strings.HasSuffix(pkgFileName, ".zip") {
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome.yaml")
}
binName := "AdGuardHome"
if runtime.GOOS == "windows" {
binName = "AdGuardHome.exe"
}
u.curBinName = filepath.Join(workDir, binName)
u.bkpBinName = filepath.Join(u.backupDir, binName)
u.newBinName = filepath.Join(u.updateDir, "AdGuardHome", binName)
if strings.HasSuffix(pkgFileName, ".zip") {
u.newBinName = filepath.Join(u.updateDir, binName)
}
return &u, nil
}
// Unpack all files from .zip file to the specified directory
// Existing files are overwritten
// Return the list of files (not directories) written
func zipFileUnpack(zipfile, outdir string) ([]string, error) {
r, err := zip.OpenReader(zipfile)
if err != nil {
return nil, fmt.Errorf("zip.OpenReader(): %s", err)
}
defer r.Close()
var files []string
var err2 error
var zr io.ReadCloser
for _, zf := range r.File {
zr, err = zf.Open()
if err != nil {
err2 = fmt.Errorf("zip file Open(): %s", err)
break
}
fi := zf.FileInfo()
if len(fi.Name()) == 0 {
continue
}
fn := filepath.Join(outdir, fi.Name())
if fi.IsDir() {
err = os.Mkdir(fn, fi.Mode())
if err != nil && !os.IsExist(err) {
err2 = fmt.Errorf("os.Mkdir(): %s", err)
break
}
log.Tracef("created directory %s", fn)
continue
}
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
if err != nil {
err2 = fmt.Errorf("os.OpenFile(): %s", err)
break
}
_, err = io.Copy(f, zr)
if err != nil {
f.Close()
err2 = fmt.Errorf("io.Copy(): %s", err)
break
}
f.Close()
log.Tracef("created file %s", fn)
files = append(files, fi.Name())
}
zr.Close()
return files, err2
}
// Unpack all files from .tar.gz file to the specified directory
// Existing files are overwritten
// Return the list of files (not directories) written
func targzFileUnpack(tarfile, outdir string) ([]string, error) {
f, err := os.Open(tarfile)
if err != nil {
return nil, fmt.Errorf("os.Open(): %s", err)
}
defer f.Close()
gzReader, err := gzip.NewReader(f)
if err != nil {
return nil, fmt.Errorf("gzip.NewReader(): %s", err)
}
var files []string
var err2 error
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
err2 = nil
break
}
if err != nil {
err2 = fmt.Errorf("tarReader.Next(): %s", err)
break
}
if len(header.Name) == 0 {
continue
}
fn := filepath.Join(outdir, header.Name)
if header.Typeflag == tar.TypeDir {
err = os.Mkdir(fn, os.FileMode(header.Mode&0777))
if err != nil && !os.IsExist(err) {
err2 = fmt.Errorf("os.Mkdir(%s): %s", fn, err)
break
}
log.Tracef("created directory %s", fn)
continue
} else if header.Typeflag != tar.TypeReg {
log.Tracef("%s: unknown file type %d, skipping", header.Name, header.Typeflag)
continue
}
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode&0777))
if err != nil {
err2 = fmt.Errorf("os.OpenFile(%s): %s", fn, err)
break
}
_, err = io.Copy(f, tarReader)
if err != nil {
f.Close()
err2 = fmt.Errorf("io.Copy(): %s", err)
break
}
f.Close()
log.Tracef("created file %s", fn)
files = append(files, header.Name)
}
gzReader.Close()
return files, err2
}
func copySupportingFiles(files []string, srcdir, dstdir string, useSrcNameOnly, useDstNameOnly bool) error {
for _, f := range files {
_, name := filepath.Split(f)
if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" {
continue
}
src := filepath.Join(srcdir, f)
if useSrcNameOnly {
src = filepath.Join(srcdir, name)
}
dst := filepath.Join(dstdir, f)
if useDstNameOnly {
dst = filepath.Join(dstdir, name)
}
err := copyFile(src, dst)
if err != nil && !os.IsNotExist(err) {
return err
}
log.Tracef("Copied: %s -> %s", src, dst)
}
return nil
}
// Download package file and save it to disk
func getPackageFile(u *updateInfo) error {
resp, err := client.Get(u.pkgURL)
if err != nil {
return fmt.Errorf("HTTP request failed: %s", err)
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
log.Tracef("Reading HTTP body")
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("ioutil.ReadAll() failed: %s", err)
}
log.Tracef("Saving package to file")
err = ioutil.WriteFile(u.pkgName, body, 0644)
if err != nil {
return fmt.Errorf("ioutil.WriteFile() failed: %s", err)
}
return nil
}
// Perform an update procedure
func doUpdate(u *updateInfo) error {
log.Info("Updating from %s to %s. URL:%s Package:%s",
VersionString, u.newVer, u.pkgURL, u.pkgName)
_ = os.Mkdir(u.updateDir, 0755)
var err error
err = getPackageFile(u)
if err != nil {
return err
}
log.Tracef("Unpacking the package")
_, file := filepath.Split(u.pkgName)
var files []string
if strings.HasSuffix(file, ".zip") {
files, err = zipFileUnpack(u.pkgName, u.updateDir)
if err != nil {
return fmt.Errorf("zipFileUnpack() failed: %s", err)
}
} else if strings.HasSuffix(file, ".tar.gz") {
files, err = targzFileUnpack(u.pkgName, u.updateDir)
if err != nil {
return fmt.Errorf("targzFileUnpack() failed: %s", err)
}
} else {
return fmt.Errorf("Unknown package extension")
}
log.Tracef("Checking configuration")
err = copyFile(u.configName, u.updateConfigName)
if err != nil {
return fmt.Errorf("copyFile() failed: %s", err)
}
cmd := exec.Command(u.newBinName, "--check-config")
err = cmd.Run()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
}
log.Tracef("Backing up the current configuration")
_ = os.Mkdir(u.backupDir, 0755)
err = copyFile(u.configName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
if err != nil {
return fmt.Errorf("copyFile() failed: %s", err)
}
// ./README.md -> backup/README.md
err = copySupportingFiles(files, config.ourWorkingDir, u.backupDir, true, true)
if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
config.ourWorkingDir, u.backupDir, err)
}
// update/[AdGuardHome/]README.md -> ./README.md
err = copySupportingFiles(files, u.updateDir, config.ourWorkingDir, false, true)
if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
u.updateDir, config.ourWorkingDir, err)
}
log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName)
err = os.Rename(u.curBinName, u.bkpBinName)
if err != nil {
return err
}
if runtime.GOOS == "windows" {
// rename fails with "File in use" error
err = copyFile(u.newBinName, u.curBinName)
} else {
err = os.Rename(u.newBinName, u.curBinName)
}
if err != nil {
return err
}
log.Tracef("Renamed: %s -> %s", u.newBinName, u.curBinName)
_ = os.Remove(u.pkgName)
_ = os.RemoveAll(u.updateDir)
return nil
}
// Complete an update procedure
func finishUpdate(u *updateInfo) {
log.Info("Stopping all tasks")
cleanup()
stopHTTPServer()
cleanupAlways()
if runtime.GOOS == "windows" {
if config.runningAsService {
// Note:
// we can't restart the service via "kardianos/service" package - it kills the process first
// we can't start a new instance - Windows doesn't allow it
cmd := exec.Command("cmd", "/c", "net stop AdGuardHome & net start AdGuardHome")
err := cmd.Start()
if err != nil {
log.Fatalf("exec.Command() failed: %s", err)
}
os.Exit(0)
}
cmd := exec.Command(u.curBinName, os.Args[1:]...)
log.Info("Restarting: %v", cmd.Args)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
log.Fatalf("exec.Command() failed: %s", err)
}
os.Exit(0)
} else {
log.Info("Restarting: %v", os.Args)
err := syscall.Exec(u.curBinName, os.Args, os.Environ())
if err != nil {
log.Fatalf("syscall.Exec() failed: %s", err)
}
// Unreachable code
}
}
// Perform an update procedure to the latest available version
func handleUpdate(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
if len(versionCheckJSON) == 0 {
httpError(w, http.StatusBadRequest, "/update request isn't allowed now")
return
}
u, err := getUpdateInfo(versionCheckJSON)
if err != nil {
httpError(w, http.StatusInternalServerError, "%s", err)
return
}
err = doUpdate(u)
if err != nil {
httpError(w, http.StatusInternalServerError, "%s", err)
return
}
returnOK(w)
time.Sleep(time.Second) // wait (hopefully) until response is sent (not sure whether it's really necessary)
go finishUpdate(u)
}

54
control_update_test.go Normal file
View File

@@ -0,0 +1,54 @@
// +build ignore
package main
import (
"os"
"testing"
)
func TestDoUpdate(t *testing.T) {
config.DNS.Port = 0
config.ourWorkingDir = "."
u := updateInfo{
pkgURL: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.95/AdGuardHome_v0.95_linux_amd64.tar.gz",
pkgName: "./AdGuardHome_v0.95_linux_amd64.tar.gz",
newVer: "v0.95",
updateDir: "./agh-update-v0.95",
backupDir: "./agh-backup-v0.94",
configName: "./AdGuardHome.yaml",
updateConfigName: "./agh-update-v0.95/AdGuardHome/AdGuardHome.yaml",
curBinName: "./AdGuardHome",
bkpBinName: "./agh-backup-v0.94/AdGuardHome",
newBinName: "./agh-update-v0.95/AdGuardHome/AdGuardHome",
}
e := doUpdate(&u)
if e != nil {
t.Fatalf("FAILED: %s", e)
}
os.RemoveAll(u.backupDir)
}
func TestTargzFileUnpack(t *testing.T) {
fn := "./dist/AdGuardHome_v0.95_linux_amd64.tar.gz"
outdir := "./test-unpack"
_ = os.Mkdir(outdir, 0755)
files, e := targzFileUnpack(fn, outdir)
if e != nil {
t.Fatalf("FAILED: %s", e)
}
t.Logf("%v", files)
os.RemoveAll(outdir)
}
func TestZipFileUnpack(t *testing.T) {
fn := "./dist/AdGuardHome_v0.95_Windows_amd64.zip"
outdir := "./test-unpack"
_ = os.Mkdir(outdir, 0755)
files, e := zipFileUnpack(fn, outdir)
if e != nil {
t.Fatalf("FAILED: %s", e)
}
t.Logf("%v", files)
os.RemoveAll(outdir)
}

322
dhcp.go
View File

@@ -2,37 +2,51 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
var dhcpServer = dhcpd.Server{}
// []dhcpd.Lease -> JSON
func convertLeases(inputLeases []dhcpd.Lease, includeExpires bool) []map[string]string {
leases := []map[string]string{}
for _, l := range inputLeases {
lease := map[string]string{
"mac": l.HWAddr.String(),
"ip": l.IP.String(),
"hostname": l.Hostname,
}
if includeExpires {
lease["expires"] = l.Expiry.Format(time.RFC3339)
}
leases = append(leases, lease)
}
return leases
}
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 {
lease := map[string]string{
"mac": rawLeases[i].HWAddr.String(),
"ip": rawLeases[i].IP.String(),
"hostname": rawLeases[i].Hostname,
"expires": rawLeases[i].Expiry.Format(time.RFC3339),
}
leases = append(leases, lease)
}
leases := convertLeases(dhcpServer.Leases(), true)
staticLeases := convertLeases(dhcpServer.StaticLeases(), false)
status := map[string]interface{}{
"config": config.DHCP,
"leases": leases,
"config": config.DHCP,
"leases": leases,
"static_leases": staticLeases,
}
w.Header().Set("Content-Type", "application/json")
@@ -43,29 +57,62 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
}
}
type leaseJSON struct {
HWAddr string `json:"mac"`
IP string `json:"ip"`
Hostname string `json:"hostname"`
}
type dhcpServerConfigJSON struct {
dhcpd.ServerConfig `json:",inline"`
StaticLeases []leaseJSON `json:"static_leases"`
}
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
newconfig := dhcpd.ServerConfig{}
newconfig := dhcpServerConfigJSON{}
err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
return
}
err = dhcpServer.CheckConfig(newconfig.ServerConfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
}
err = dhcpServer.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
err = dhcpServer.Init(newconfig.ServerConfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
}
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()
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
}
}
config.DHCP = newconfig
config.DHCP = newconfig.ServerConfig
httpUpdateConfigReloadDNSReturnOK(w, r)
}
@@ -130,6 +177,10 @@ 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)
@@ -147,13 +198,35 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
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 {
@@ -162,12 +235,211 @@ 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 handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
lj := leaseJSON{}
err := json.NewDecoder(r.Body).Decode(&lj)
if err != nil {
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
ip := parseIPv4(lj.IP)
if ip == nil {
httpError(w, http.StatusBadRequest, "invalid IP")
return
}
mac, _ := net.ParseMAC(lj.HWAddr)
lease := dhcpd.Lease{
IP: ip,
HWAddr: mac,
Hostname: lj.Hostname,
}
err = dhcpServer.AddStaticLease(lease)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
returnOK(w)
}
func handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
lj := leaseJSON{}
err := json.NewDecoder(r.Body).Decode(&lj)
if err != nil {
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
ip := parseIPv4(lj.IP)
if ip == nil {
httpError(w, http.StatusBadRequest, "invalid IP")
return
}
mac, _ := net.ParseMAC(lj.HWAddr)
lease := dhcpd.Lease{
IP: ip,
HWAddr: mac,
Hostname: lj.Hostname,
}
err = dhcpServer.RemoveStaticLease(lease)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
returnOK(w)
}
func startDHCPServer() error {
if !config.DHCP.Enabled {
// not enabled, don't do anything
return nil
}
err := dhcpServer.Start(&config.DHCP)
err := dhcpServer.Init(config.DHCP)
if err != nil {
return errorx.Decorate(err, "Couldn't init DHCP server")
}
err = dhcpServer.Start()
if err != nil {
return errorx.Decorate(err, "Couldn't start DHCP server")
}
@@ -179,10 +451,6 @@ func stopDHCPServer() error {
return nil
}
if !dhcpServer.Enabled {
return nil
}
err := dhcpServer.Stop()
if err != nil {
return errorx.Decorate(err, "Couldn't stop DHCP server")

View File

@@ -1,6 +1,7 @@
package dhcpd
import (
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
@@ -11,6 +12,7 @@ import (
"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
}
}

View File

@@ -23,8 +23,20 @@ type leaseJSON struct {
Expiry int64 `json:"exp"`
}
// Safe version of dhcp4.IPInRange()
func ipInRange(start, stop, ip net.IP) bool {
if len(start) != len(stop) ||
len(start) != len(ip) {
return false
}
return dhcp4.IPInRange(start, stop, ip)
}
// Load lease table from DB
func (s *Server) dbLoad() {
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
data, err := ioutil.ReadFile(dbFilename)
if err != nil {
if !os.IsNotExist(err) {
@@ -40,13 +52,12 @@ func (s *Server) dbLoad() {
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) {
if obj[i].Expiry != leaseExpireStatic &&
!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
}

View File

@@ -14,6 +14,7 @@ import (
)
const defaultDiscoverTime = time.Second * 3
const leaseExpireStatic = 1
// Lease contains the necessary information about a DHCP lease
// field ordering is important -- yaml fields will mirror ordering from here
@@ -21,7 +22,10 @@ type Lease struct {
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
IP net.IP `json:"ip"`
Hostname string `json:"hostname"`
Expiry time.Time `json:"expires"`
// Lease expiration time
// 1: static lease
Expiry time.Time `json:"expires"`
}
// ServerConfig - DHCP server configuration
@@ -53,6 +57,7 @@ type Server struct {
// leases
leases []*Lease
leasesLock sync.RWMutex
leaseStart net.IP // parsed from config RangeStart
leaseStop net.IP // parsed from config RangeEnd
leaseTime time.Duration // parsed from config LeaseDuration
@@ -61,8 +66,7 @@ type Server struct {
// IP address pool -- if entry is in the pool, then it's attached to a lease
IPpool map[[4]byte]net.HardwareAddr
ServerConfig
sync.RWMutex
conf ServerConfig
}
// Print information about the available network interfaces
@@ -75,62 +79,65 @@ func printInterfaces() {
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 {
if config != nil {
s.ServerConfig = *config
}
// CheckConfig checks the configuration
func (s *Server) CheckConfig(config ServerConfig) error {
tmpServer := Server{}
return tmpServer.setConfig(config)
}
iface, err := net.InterfaceByName(s.InterfaceName)
// Init checks the configuration and initializes the server
func (s *Server) Init(config ServerConfig) error {
err := s.setConfig(config)
if err != nil {
return err
}
s.dbLoad()
return nil
}
func (s *Server) setConfig(config ServerConfig) error {
s.conf = config
iface, err := net.InterfaceByName(config.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)
return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName)
}
// get ipv4 address of an interface
s.ipnet = getIfaceIPv4(iface)
if s.ipnet == nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface)
}
if s.LeaseDuration == 0 {
if config.LeaseDuration == 0 {
s.leaseTime = time.Hour * 2
s.LeaseDuration = uint(s.leaseTime.Seconds())
} else {
s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
s.leaseTime = time.Second * time.Duration(config.LeaseDuration)
}
s.leaseStart, err = parseIPv4(s.RangeStart)
s.leaseStart, err = parseIPv4(config.RangeStart)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart)
}
s.leaseStop, err = parseIPv4(s.RangeEnd)
s.leaseStop, err = parseIPv4(config.RangeEnd)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd)
}
subnet, err := parseIPv4(s.SubnetMask)
subnet, err := parseIPv4(config.SubnetMask)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask)
}
// if !bytes.Equal(subnet, s.ipnet.Mask) {
// s.closeConn() // in case it was already started
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
// }
router, err := parseIPv4(s.GatewayIP)
router, err := parseIPv4(config.GatewayIP)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP)
}
s.leaseOptions = dhcp4.Options{
@@ -139,12 +146,21 @@ func (s *Server) Start(config *ServerConfig) error {
dhcp4.OptionDomainNameServer: s.ipnet.IP,
}
return nil
}
// Start will listen on port 67 and serve DHCP requests.
func (s *Server) Start() error {
// TODO: don't close if interface and addresses are the same
if s.conn != nil {
s.closeConn()
}
s.dbLoad()
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
}
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 {
@@ -229,9 +245,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
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.leasesLock.Lock()
s.leases[i] = lease
s.Unlock()
s.leasesLock.Unlock()
s.reserveIP(lease.IP, hwaddr)
return lease, nil
@@ -239,9 +255,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
lease.IP = ip
s.Lock()
s.leasesLock.Lock()
s.leases = append(s.leases, lease)
s.Unlock()
s.leasesLock.Unlock()
return lease, nil
}
@@ -261,7 +277,7 @@ func (s *Server) findLease(p dhcp4.Packet) *Lease {
func (s *Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() <= now {
if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic {
return i
}
}
@@ -269,11 +285,6 @@ func (s *Server) findExpiredLease() int {
}
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
// if IP pool is nil, lazy initialize it
if s.IPpool == nil {
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}
// go from start to end, find unreserved IP
var foundIP net.IP
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
@@ -361,7 +372,7 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
// 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 {
if s.conf.ICMPTimeout == 0 {
return true
}
@@ -372,7 +383,7 @@ func (s *Server) addrAvailable(target net.IP) bool {
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
@@ -395,11 +406,11 @@ func (s *Server) addrAvailable(target net.IP) bool {
func (s *Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.reserveIP(lease.IP, hw)
s.Lock()
s.leasesLock.Lock()
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.leaseTime)
s.Unlock()
s.leasesLock.Unlock()
}
// Return TRUE if DHCP packet is correct
@@ -516,21 +527,103 @@ func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pack
return nil
}
// AddStaticLease adds a static lease (thread-safe)
func (s *Server) AddStaticLease(l Lease) error {
if s.IPpool == nil {
return fmt.Errorf("DHCP server isn't started")
}
if len(l.IP) != 4 {
return fmt.Errorf("Invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("Invalid MAC")
}
l.Expiry = time.Unix(leaseExpireStatic, 0)
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if s.findReservedHWaddr(l.IP) != nil {
return fmt.Errorf("IP is already used")
}
s.leases = append(s.leases, &l)
s.reserveIP(l.IP, l.HWAddr)
s.dbStore()
return nil
}
// RemoveStaticLease removes a static lease (thread-safe)
func (s *Server) RemoveStaticLease(l Lease) error {
if s.IPpool == nil {
return fmt.Errorf("DHCP server isn't started")
}
if len(l.IP) != 4 {
return fmt.Errorf("Invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("Invalid MAC")
}
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
if s.findReservedHWaddr(l.IP) == nil {
return fmt.Errorf("Lease not found")
}
var newLeases []*Lease
for _, lease := range s.leases {
if bytes.Equal(lease.IP.To4(), l.IP) {
if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
lease.Hostname != l.Hostname {
return fmt.Errorf("Lease not found")
}
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
s.unreserveIP(l.IP)
s.dbStore()
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()
s.leasesLock.RLock()
for _, lease := range s.leases {
if lease.Expiry.Unix() > now {
result = append(result, *lease)
}
}
s.RUnlock()
s.leasesLock.RUnlock()
return result
}
// StaticLeases returns the list of statically-configured DHCP leases (thread-safe)
func (s *Server) StaticLeases() []Lease {
s.leasesLock.Lock()
if s.IPpool == nil {
s.dbLoad()
}
s.leasesLock.Unlock()
var result []Lease
s.leasesLock.RLock()
for _, lease := range s.leases {
if lease.Expiry.Unix() == 1 {
result = append(result, *lease)
}
}
s.leasesLock.RUnlock()
return result
}
// Print information about the current leases
func (s *Server) printLeases() {
log.Tracef("Leases:")
@@ -540,10 +633,23 @@ func (s *Server) printLeases() {
}
}
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
now := time.Now().Unix()
s.leasesLock.RLock()
defer s.leasesLock.RUnlock()
for _, l := range s.leases {
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
return l.IP
}
}
return nil
}
// Reset internal state
func (s *Server) reset() {
s.Lock()
s.leasesLock.Lock()
s.leases = nil
s.Unlock()
s.leasesLock.Unlock()
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}

View File

@@ -26,6 +26,7 @@ func TestDHCP(t *testing.T) {
var lease *Lease
var opt dhcp4.Options
s.reset()
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
@@ -132,6 +133,7 @@ func TestDB(t *testing.T) {
var hw1, hw2 net.HardwareAddr
var lease *Lease
s.reset()
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second

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
}

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