Compare commits

...

203 Commits

Author SHA1 Message Date
Andrey Meshkov
aa30728cda Fix docker build 2019-02-25 19:15:27 +03:00
Andrey Meshkov
71ab95f12f Bump version to v0.93 2019-02-25 19:04:41 +03:00
Andrey Meshkov
c71d6ed433 Fix race in safesearch tests 2019-02-25 18:56:51 +03:00
Andrey Meshkov
77348e746f Merge pull request #151 in DNS/adguard-dns from fix/576 to master
* commit '86279f19b0b5bce37a83c05c88511fb82441c730':
  Add TODO
  Fix merge issues
  Add stats assertions
  Add safesearch test for dnsforward
  Use go tools
  Fix #576 - Fix safesearch
2019-02-25 18:27:17 +03:00
Andrey Meshkov
27c33b2fa9 Merge pull request #152 in DNS/adguard-dns from feature/update_readme to master
* commit '4bbc5037098636d9c68ae41391315da3737404a5':
  Added guides to readme
  update readme
2019-02-25 17:23:22 +03:00
Aleksey Dmitrevskiy
86279f19b0 Add TODO 2019-02-25 17:15:50 +03:00
Aleksey Dmitrevskiy
3d901a82ad Fix merge issues 2019-02-25 17:07:26 +03:00
Aleksey Dmitrevskiy
d351ed82c1 Merge branch 'master' into fix/576 2019-02-25 17:07:02 +03:00
Aleksey Dmitrevskiy
8e13f22aa5 Add stats assertions 2019-02-25 17:01:57 +03:00
Aleksey Dmitrevskiy
d0f4f22e0d Add safesearch test for dnsforward 2019-02-25 14:58:54 +03:00
Andrey Meshkov
4bbc503709 Added guides to readme 2019-02-24 20:13:58 +03:00
Andrey Meshkov
1b305d94a7 update readme 2019-02-24 18:42:44 +03:00
Andrey Meshkov
a7478255a1 Merge pull request #148 in DNS/adguard-dns from feature/285 to master
* commit '84604e292b7c80980dc51a4692715350e93590c9': (88 commits)
  Update dnsproxy to v0.11.1
  Fix printing HTTPS address
  Fix tests
  Update dnsproxy to 0.11.0
  Added install methods to openapi.yaml Print all net interfaces when bind_host is 0.0.0.0
  Added DOH url
  Added DNS-over-TLS unit-test and a test looking for race-conditions
  Fixed port validation
  Fix data races found by race detector.
  /tls/configure -- don't close https connection mid-request when configuration removes ports and certificates
  Fixed checkRedirect helper
  Added new config fields to readme
  Added openapi description
  Fixed EncryptionTopline check
  Fixed stylelint errors
  Added UpdateTopline component
  Remove unused package
  Fixed npm audit vulnerabilities
  Fix empty values on validate
  Disable save button if key or certificate is empty
  ...
2019-02-22 19:48:10 +03:00
Andrey Meshkov
84604e292b Update dnsproxy to v0.11.1 2019-02-22 19:45:43 +03:00
Andrey Meshkov
a04923a4f3 Fix printing HTTPS address 2019-02-22 18:47:54 +03:00
Andrey Meshkov
1da954fa97 Fix tests 2019-02-22 18:41:59 +03:00
Andrey Meshkov
ad4b58472f Update dnsproxy to 0.11.0 2019-02-22 18:16:47 +03:00
Andrey Meshkov
4e1c1618cb Added install methods to openapi.yaml
Print all net interfaces when bind_host is 0.0.0.0
2019-02-22 17:59:42 +03:00
Aleksey Dmitrevskiy
3916f1073d Use go tools 2019-02-22 16:41:30 +03:00
Aleksey Dmitrevskiy
623c3bba09 Fix #576 - Fix safesearch 2019-02-22 16:34:36 +03:00
Andrey Meshkov
e8898811fe Added DOH url 2019-02-22 15:52:12 +03:00
Andrey Meshkov
71df659dc9 Added DNS-over-TLS unit-test and a test looking for race-conditions 2019-02-22 15:23:39 +03:00
Ildar Kamalov
158f2f6100 Fixed port validation 2019-02-21 19:16:09 +03:00
Eugene Bujak
8e993cd788 Fix data races found by race detector. 2019-02-21 19:07:12 +03:00
Eugene Bujak
12f8590228 /tls/configure -- don't close https connection mid-request when configuration removes ports and certificates 2019-02-21 19:01:20 +03:00
Ildar Kamalov
2814c393ad Fixed checkRedirect helper 2019-02-21 18:28:23 +03:00
Andrey Meshkov
37431735fd Added new config fields to readme 2019-02-21 17:48:18 +03:00
Andrey Meshkov
251beb24d3 Added openapi description 2019-02-21 17:33:46 +03:00
Ildar Kamalov
37a1a98c49 Fixed EncryptionTopline check 2019-02-21 15:39:15 +03:00
Ildar Kamalov
5ac775aa4a Fixed stylelint errors 2019-02-21 15:05:54 +03:00
Ildar Kamalov
c53a132072 Added UpdateTopline component 2019-02-20 16:54:14 +03:00
Ildar Kamalov
8e7ceec1a1 Remove unused package 2019-02-20 16:10:32 +03:00
Ildar Kamalov
89446fccd5 Fixed npm audit vulnerabilities 2019-02-20 15:39:36 +03:00
Ildar Kamalov
4f45f2c3e3 Fix empty values on validate 2019-02-20 14:26:56 +03:00
Ildar Kamalov
9c8e4c64ea Disable save button if key or certificate is empty 2019-02-20 13:33:42 +03:00
Ildar Kamalov
2c2295c161 Fixed http port and reset with save 2019-02-20 12:46:34 +03:00
Eugene Bujak
a2dd7c32d5 /tls/ -- move certificate logging to verbose 2019-02-20 12:32:10 +03:00
Eugene Bujak
b3f33b4b0b /status -- add http_port 2019-02-20 12:25:13 +03:00
Ildar Kamalov
de08b53ae1 Fix list styles 2019-02-20 12:02:46 +03:00
Ildar Kamalov
a60eeb55f1 Check if redirect is available before enable 2019-02-20 11:36:24 +03:00
Eugene Bujak
9d4b829fb6 Add Access-Control-Allow-Origin: * header to postinstall wrapper 2019-02-20 10:40:18 +03:00
Eugene Bujak
1515c353f8 implement redirecting to https if configured and https server is running 2019-02-19 21:19:27 +03:00
Ildar Kamalov
f0536b6347 Check response 2019-02-19 19:42:59 +03:00
Ildar Kamalov
340a4fb58e Check if redirect available 2019-02-19 19:19:40 +03:00
Eugene Bujak
e873149bee Fix inability to start https server if it wasn't running 2019-02-19 19:11:39 +03:00
Ildar Kamalov
77793e5f21 Check is safe port 2019-02-19 18:56:13 +03:00
Ildar Kamalov
24154f0033 Redirect on port change 2019-02-19 18:04:23 +03:00
Eugene Bujak
8c406427af /tls/configure -- accept empty certificates for saving 2019-02-19 17:52:27 +03:00
Eugene Bujak
885e4e16c8 /tls/ -- prevent encryption errors when changing certificates mid-request 2019-02-19 17:52:19 +03:00
Ildar Kamalov
0b7f0396de Fixed processing config 2019-02-19 15:46:29 +03:00
Ildar Kamalov
cca6998efe Added https redirect 2019-02-19 15:43:36 +03:00
Eugene Bujak
3c374b5940 /tls/ -- add internal usable flag to simplify logic when https needs to be booted up 2019-02-19 15:21:38 +03:00
Eugene Bujak
ba103f9825 /tls/ -- add ValidCert, without it being true https is not usable 2019-02-19 15:21:19 +03:00
Eugene Bujak
2748d4c889 /tls/configure -- check if https port is usable before accepting the new config 2019-02-19 15:19:11 +03:00
Ildar Kamalov
b8c0ed9335 Reset fields on click 2019-02-19 13:05:16 +03:00
Ildar Kamalov
ff012cf0a3 Fix error message 2019-02-19 11:05:30 +03:00
Ildar Kamalov
2b0addd505 Fix copy symbol
Closes #588
2019-02-19 11:04:43 +03:00
Eugene Bujak
2de0f82bbc release.sh -- don't require directory of this repo to be named specifically 2019-02-18 21:13:58 +03:00
Ildar Kamalov
1fc5f15aaa Check if error has response 2019-02-18 19:36:24 +03:00
Ildar Kamalov
954d923975 Remove valid_chain and warning_validation from button disable 2019-02-18 19:20:17 +03:00
Ildar Kamalov
05cce8b107 Added validation on change and enable encryption checkbox 2019-02-18 16:06:27 +03:00
Eugene Bujak
d44f68e844 /tls/configure and /tls/validate -- make validation failures non-fatal 2019-02-15 17:07:45 +03:00
Eugene Bujak
cb97c221fd /tls/validate and /tls/configure -- do checks on private key, add more fields to certificate status, do keypair check last. 2019-02-15 16:28:28 +03:00
Eugene Bujak
81bb4aea78 /tls/configure and /tls/status -- now there's an explicit 'enabled' bool. 2019-02-15 16:28:28 +03:00
Eugene Bujak
8da90a7f4a Fix panic when https server is not running 2019-02-15 16:28:28 +03:00
Eugene Bujak
b4b800565c Fixup for "validate certificates". 2019-02-15 16:28:28 +03:00
Eugene Bujak
e8280c60d8 /tls/status — Add not_after field with a valid certificate expiration date. 2019-02-15 16:28:28 +03:00
Eugene Bujak
571be68733 Validate certificates and update certificate statuses on launch as well. 2019-02-15 16:28:28 +03:00
Eugene Bujak
bdec98f18e Properly calculate if certificate expires in 30 minutes or not. 2019-02-15 16:28:28 +03:00
Eugene Bujak
28df187012 /tls/configure -- restart HTTPS server if settings changed
Fixes not using new HTTPS certificate after submitting it.
2019-02-15 16:28:28 +03:00
Eugene Bujak
f0569af367 Remove redundant printf 2019-02-15 16:28:28 +03:00
Eugene Bujak
e2956cae82 release.sh -- Place the targz into dist subdir 2019-02-15 16:28:28 +03:00
Eugene Bujak
110434c2d5 Fix broken tar.gz not having a subdirectory inside. 2019-02-15 16:28:28 +03:00
Eugene Bujak
f417f6257f release.sh -- there is no need to run make clean 2019-02-15 16:28:28 +03:00
Eugene Bujak
1d2958f4aa add temporary packr output to gitignore 2019-02-15 16:28:28 +03:00
Eugene Bujak
3e67c8d79a Older npm rewrote the package-lock.json again 2019-02-15 16:28:28 +03:00
Eugene Bujak
57a33654f7 Certificate that doesn't go through the chain is not fatal, just send the warning over json. 2019-02-15 16:28:28 +03:00
Eugene Bujak
30050bf278 Spin up an HTTPS server when certificates, port and private key are configured. 2019-02-15 16:28:28 +03:00
Eugene Bujak
5cbaeb82a8 Introduce /tls/validate and validateCertificates() that will also be used by /tls/configure 2019-02-15 16:28:28 +03:00
Eugene Bujak
876bec5a65 /tls/configure -- introduce unmarshalTLS() that transparently base64-decodes the certificate 2019-02-15 16:28:28 +03:00
Eugene Bujak
4b4faad9e8 Fix status for certificates not updating. 2019-02-15 16:28:28 +03:00
Eugene Bujak
c061bec6d8 Lower down logging noise when idle. 2019-02-15 16:28:28 +03:00
Eugene Bujak
229ef78085 Activate DNS-over-TLS server when certificates, keys and ports are configured. 2019-02-15 16:28:28 +03:00
Eugene Bujak
0aeca6bbf5 Don't keep certificates and keys encoded with base64 in yaml config 2019-02-15 16:28:28 +03:00
Ildar Kamalov
35b5f4b48b Fixed json and updated zh_tw 2019-02-15 16:28:28 +03:00
Eugene Bujak
0d3aa00956 Default values for DoH and DoT ports 2019-02-15 16:28:28 +03:00
Ildar Kamalov
cb9ffe4de9 Send 0 on empty port value 2019-02-15 16:28:28 +03:00
Ildar Kamalov
351673c060 Initial port values 2019-02-15 16:28:28 +03:00
Eugene Bujak
4a14c199d8 /tls/configure -- allow submitting empty certificates and keys to clear them out from config 2019-02-15 16:28:28 +03:00
Ildar Kamalov
1dd548c36c Added button to reset encryption settings 2019-02-15 16:28:28 +03:00
Eugene Bujak
d42718465d /tls/configure -- certificates/keys are now transferred encoded with base64 2019-02-15 16:28:28 +03:00
Ildar Kamalov
93847bd309 Convert certificate and key to base64 2019-02-15 16:28:28 +03:00
Eugene Bujak
4da55dc2aa Fixup of previous commit -- fix build failure 2019-02-15 16:28:27 +03:00
Eugene Bujak
3d3e0784ea tls/configure -- Backend implementation of parsing user certs 2019-02-15 16:28:27 +03:00
Ildar Kamalov
3898309778 Request tls status after save 2019-02-15 16:28:27 +03:00
Eugene Bujak
c19416bf8e Move up tls block in config, don't send json with zero values 2019-02-15 16:28:27 +03:00
Ildar Kamalov
c025c845d2 Show random status and warning 2019-02-15 16:28:27 +03:00
Eugene Bujak
c5b1105fc1 /tls/status -- Expand random stubs for separate statuses of certificate and key 2019-02-15 16:28:27 +03:00
Eugene Bujak
38869b22a6 tls/status -- make stubs add warning and status randomly 2019-02-15 16:28:27 +03:00
Ildar Kamalov
ab11c912db Added topline component and fixed string interpolation 2019-02-15 16:28:27 +03:00
Ildar Kamalov
7451eb1346 Initial components for encryption settings 2019-02-15 16:28:27 +03:00
Eugene Bujak
8725c1df7a Add stub OpenAPI methods 2019-02-15 16:28:26 +03:00
Eugene Bujak
0820983d81 go.mod -- update dnsproxy to v0.9.11 and it's dependencies 2019-02-15 16:28:26 +03:00
Eugene Bujak
a5b61459cc Merge pull request #150 in DNS/adguard-dns from fix/582 to master
* commit 'dd3621bcf65df76fa866866edb8410f5aea46e2a':
  Fix #582
2019-02-12 15:02:03 +03:00
Andrey Meshkov
dd3621bcf6 Fix #582 2019-02-12 14:46:44 +03:00
Eugene Bujak
571370ab16 Merge pull request #149 in DNS/adguard-dns from docker-expose to master
* commit 'e33c8a3cde35ac06a34099ecd94b29b4d9721744':
  Add exposed ports
2019-02-12 13:22:22 +03:00
Eugene Zbiranik
e33c8a3cde Add exposed ports 2019-02-12 11:54:40 +03:00
Eugene Bujak
0d5f24927c Merge pull request #147 in DNS/adguard-dns from docker-versions to master
* commit '27ea739cfdc782daeca07ae8af6bb8f6ef6d65b3':
  fix
  doc
  Fix to go along with new concept
  Build latest from tag branch, edge from master
2019-02-11 19:44:24 +03:00
Eugene Zbiranik
27ea739cfd fix 2019-02-11 19:38:45 +03:00
Eugene Zbiranik
899b26725e doc 2019-02-11 16:56:11 +03:00
Eugene Bujak
26f2207b5c Merge pull request #146 in DNS/adguard-dns from fix/579 to master
* commit 'a40ddb094b4af768ee4b78b09a4a50112eae3b2f':
  Fix review comments
  go mod tidy
  Add workdir to readme
  Do not store last_updated in the config file anymore
  Fix #579
2019-02-11 15:34:15 +03:00
Eugene Zbiranik
6d7d10ec38 Fix to go along with new concept 2019-02-11 15:17:49 +03:00
Eugene Zbiranik
c1f6da2b52 Build latest from tag branch, edge from master 2019-02-11 15:10:36 +03:00
Andrey Meshkov
a40ddb094b Fix review comments 2019-02-11 14:22:36 +03:00
Andrey Meshkov
b477b67428 go mod tidy 2019-02-10 21:52:29 +03:00
Andrey Meshkov
cd9db6440b Add workdir to readme 2019-02-10 21:50:55 +03:00
Andrey Meshkov
9ff420bb52 Do not store last_updated in the config file anymore 2019-02-10 21:44:16 +03:00
Andrey Meshkov
9a03190a62 Fix #579
1. Added --workdir command-line argument that lets configure the working dir.
2. Made "dnsforward" use this workdir parameter when saving/reading querylog.
3. Reworked "dnsforward" -- moved http handlers out of there to control.go
2019-02-10 20:47:43 +03:00
Eugene Bujak
6b6eacaa2b Merge pull request #145 in DNS/adguard-dns from feature/425 to master
* commit '5ca33e44d897d08117e10b248fe9dbe25d3b31f8': (45 commits)
  Fix object spread
  Demote some log.printf into log.tracef
  Makefile -- no need for go get -d . anymore
  npm 6.7.0 unconditionally modifies package-lock.json. Commit those changes.
  /install/configure -- Don't fail if HTTP listen host and port don't change
  /install/get_addresses -- don't send link-local addresses
  Increase button width
  Disable button on submit
  Common reducer for toasts
  Check if ip_addresses exist in the interface
  Fix data race found by tests -- https://travis-ci.org/AdguardTeam/AdGuardHome/jobs/489674061#L970
  Minor cleanup, added strings, added more information to response when error occurs
  ingnore Shutdown by golangci
  Fixed custom select arrow
  Remove unused icons
  get rid of go-spew and cleanup go.mod from unused packages
  Hide 80 web port
  Default listening to 0.0.0.0 for first-time setup
  Move installation of /install handlers into a separate optional function
  /install/configure -- Rebind HTTP server when we get new host and port
  ...
2019-02-08 13:52:04 +03:00
Ildar Kamalov
5ca33e44d8 Fix object spread 2019-02-07 18:51:21 +03:00
Eugene Bujak
68c8a4d484 Demote some log.printf into log.tracef 2019-02-07 18:24:43 +03:00
Eugene Bujak
6e5731ab02 Makefile -- no need for go get -d . anymore 2019-02-07 18:24:42 +03:00
Eugene Bujak
548f539566 npm 6.7.0 unconditionally modifies package-lock.json. Commit those changes. 2019-02-07 18:24:42 +03:00
Eugene Bujak
853582dade /install/configure -- Don't fail if HTTP listen host and port don't change 2019-02-07 18:24:42 +03:00
Eugene Bujak
3a94080491 /install/get_addresses -- don't send link-local addresses 2019-02-07 18:24:42 +03:00
Ildar Kamalov
ba161e9a6f Increase button width 2019-02-07 16:21:17 +03:00
Ildar Kamalov
91eaf72051 Disable button on submit 2019-02-07 16:09:12 +03:00
Ildar Kamalov
826529e73e Common reducer for toasts 2019-02-07 15:40:26 +03:00
Ildar Kamalov
c466f8cc73 Check if ip_addresses exist in the interface 2019-02-07 15:17:27 +03:00
Eugene Bujak
f9d1948f6a Fix data race found by tests -- https://travis-ci.org/AdguardTeam/AdGuardHome/jobs/489674061#L970 2019-02-07 14:45:46 +03:00
Andrey Meshkov
bb8d7c37bb Minor cleanup, added strings, added more information to response when error occurs 2019-02-07 14:22:08 +03:00
Andrey Meshkov
f2d7f8161b ingnore Shutdown by golangci 2019-02-07 13:52:14 +03:00
Ildar Kamalov
39b2e345c3 Fixed custom select arrow 2019-02-07 11:24:23 +03:00
Ildar Kamalov
a7a38413fe Remove unused icons 2019-02-07 10:58:02 +03:00
Eugene Bujak
fe671152c2 get rid of go-spew and cleanup go.mod from unused packages 2019-02-06 20:50:17 +03:00
Ildar Kamalov
ba678ffa82 Hide 80 web port 2019-02-06 17:32:32 +03:00
Eugene Bujak
672ff33879 Default listening to 0.0.0.0 for first-time setup 2019-02-06 17:28:09 +03:00
Eugene Bujak
398312cd80 Move installation of /install handlers into a separate optional function 2019-02-06 17:28:08 +03:00
Eugene Bujak
06a28a461d /install/configure -- Rebind HTTP server when we get new host and port 2019-02-06 17:25:18 +03:00
Ildar Kamalov
31b855f9ab Show list of addresses 2019-02-06 17:22:46 +03:00
Ildar Kamalov
f379d34813 Added select for listen interfaces 2019-02-06 17:22:46 +03:00
Eugene Bujak
5abe5af707 /install/configure -- Start DNS server explicitly 2019-02-06 17:22:46 +03:00
Eugene Bujak
daae040f9c Check if IP:port combinations are possible before returning OK on /install/configure 2019-02-06 17:22:46 +03:00
Eugene Bujak
f2b3c3a14c /install/get_addresses -- made IP address omitempty 2019-02-06 17:21:23 +03:00
Eugene Bujak
d3e81c47f6 rename /install/ path names to be more fitting 2019-02-06 17:21:23 +03:00
Eugene Bujak
c14aff3dba /install/get_default_addresses -- Remove subnet suffix from addresses 2019-02-06 17:21:23 +03:00
Eugene Bujak
d97c426646 Fill out port 80 if it's available, otherwise port 3000 2019-02-06 17:21:23 +03:00
Eugene Bujak
34e14930de /install/get_default_addresses -- now it gives out list of interfaces 2019-02-06 17:21:23 +03:00
Ildar Kamalov
924afea22b Show port in interface and dns address 2019-02-06 17:21:23 +03:00
Eugene Bujak
302c3a767a Initial implementation of welcome/firstrun/installer page in go backend 2019-02-06 17:21:23 +03:00
Ildar Kamalov
c494e17df5 Update packages 2019-02-06 17:17:38 +03:00
Ildar Kamalov
7c25c0febe Added DHCP warning 2019-02-06 17:17:38 +03:00
Ildar Kamalov
5f7fc0f041 Remove 'enabled' from initial values 2019-02-06 17:17:38 +03:00
Ildar Kamalov
beb94741cf Fixed initial values and string interpolation 2019-02-06 17:17:38 +03:00
Ildar Kamalov
24be7ce4ed Fixed page reload on settings change 2019-02-06 17:17:38 +03:00
Ildar Kamalov
6e41897323 Disable form submit on error 2019-02-06 17:17:38 +03:00
Ildar Kamalov
b5e7237169 Update components on language change 2019-02-06 17:17:38 +03:00
Ildar Kamalov
7e95ce9136 Add device configuration instruction 2019-02-06 17:17:38 +03:00
Ildar Kamalov
a7416f9c34 Fixed validation and added toasts 2019-02-06 17:17:38 +03:00
Ildar Kamalov
2bd4840ba5 Fix styles 2019-02-06 17:17:38 +03:00
Ildar Kamalov
5349ec76fd Added components for web setup 2019-02-06 17:17:38 +03:00
Ildar Kamalov
71259c5f19 Added web setup entry point 2019-02-06 17:17:38 +03:00
Eugene Bujak
f21aebd1cf /install/get_default_addresses -- make fields lowercase 2019-02-06 17:17:38 +03:00
Eugene Bujak
c36a7895ad Add install page API stubs 2019-02-06 17:17:38 +03:00
Andrey Meshkov
5fed5c0718 We'd better keep -h for host 2019-02-05 23:29:11 +03:00
Andrey Meshkov
f437d53c1c Fix Dockerfile run cmd 2019-02-05 23:17:17 +03:00
Eugene Bujak
bfe25ba014 Make build_docker.sh executable. 2019-02-05 22:28:24 +03:00
Andrey Meshkov
a8cdc5b01c Merge pull request #144 in DNS/adguard-dns from feature/490 to master
* commit '0fbfa057b1e2e709357fda08caea20fcbc61e9f4':
  Get rid of hardcoded binary name
  service properties to constants
  Added logging description to README
  Fixed review comments Fixed running as a windows service Added logging to windows evenlog
  Added github.com/kardianos/service to README
  AdGuard Home as a system service
2019-02-05 22:02:01 +03:00
Andrey Meshkov
0fbfa057b1 Get rid of hardcoded binary name 2019-02-05 20:35:48 +03:00
Andrey Meshkov
93ea27077f service properties to constants 2019-02-05 14:21:07 +03:00
Andrey Meshkov
aab8da4c7c Added logging description to README 2019-02-05 14:15:22 +03:00
Andrey Meshkov
448a6caeb8 Fixed review comments
Fixed running as a windows service
Added logging to windows evenlog
2019-02-05 14:09:05 +03:00
Andrey Meshkov
a4dc4c61d8 Added github.com/kardianos/service to README 2019-02-04 13:57:35 +03:00
Andrey Meshkov
277415124e AdGuard Home as a system service
1. Reworked working with command-line arguments
2. Added service control actions: install/uninstall/start/stop/status
3. Added log settings to the configuration file
4. Updated the README file
2019-02-04 13:54:53 +03:00
Eugene Bujak
b216475c20 Merge pull request #142 in DNS/adguard-dns from feature/562 to master
* commit '09b49d01450f059ecc4649433a1510f31d2205ac':
  Add Docker Hub image builds
2019-01-30 15:07:23 +03:00
Eugene Bujak
c776ad21b7 Merge pull request #143 in DNS/adguard-dns from fix/564 to master
* commit 'b56dcc9de16db455aa5e3cf775f75c00beca9525':
  Fix version
  Add minimum supported node version
2019-01-30 15:06:43 +03:00
Ildar Kamalov
b56dcc9de1 Fix version 2019-01-30 15:05:19 +03:00
Ildar Kamalov
05cab6fde0 Add minimum supported node version
Closes #564
2019-01-30 14:59:22 +03:00
Eugene Zbiranik
09b49d0145 Merge master 2019-01-29 15:18:36 +03:00
Eugene Zbiranik
98bfb82787 Add Docker Hub image builds 2019-01-29 15:11:11 +03:00
Eugene Bujak
d238e1feb3 Merge pull request #140 in DNS/adguard-dns from fix/557_travis to master
* commit '911250cfbec46c79181bf09fd2f0f397cc7eca35':
  MORE BADGES FOR THE GOD OF BADGES!
  Closes #557: Travis deploy
2019-01-26 02:13:18 +03:00
Andrey Meshkov
911250cfbe MORE BADGES FOR THE GOD OF BADGES! 2019-01-25 22:23:09 +03:00
Andrey Meshkov
4b4cb99b30 Closes #557: Travis deploy 2019-01-25 22:12:48 +03:00
Eugene Bujak
0161509b5f Merge pull request #137 in DNS/adguard-dns from fix/557 to master
* commit 'ec6b1f7c42c8d2fc413d29fba55430b89fcbce2d':
  Added golangci-lint configuration and prepared for the integrattion
  Added codecov, goreport
  Use EnableAll in gometalinter config
  gometalinter
2019-01-25 20:14:03 +03:00
Andrey Meshkov
ec6b1f7c42 Added golangci-lint configuration and prepared for the integrattion 2019-01-25 20:13:57 +03:00
Andrey Meshkov
69a75fbcaa Added codecov, goreport 2019-01-25 20:13:57 +03:00
Andrey Meshkov
a0157e39c6 Use EnableAll in gometalinter config 2019-01-25 20:13:57 +03:00
Andrey Meshkov
d078851246 gometalinter 2019-01-25 20:13:57 +03:00
Andrey Meshkov
c9d627ea71 Merge pull request #139 in DNS/adguard-dns from fix/559 to master
* commit '297a1c7fa53910e38c041ed2c430ff27b0509213':
  Added regex example to the "Custom filtering rules" settings
2019-01-25 18:43:47 +03:00
Ildar Kamalov
297a1c7fa5 Added regex example to the "Custom filtering rules" settings
Closes #559
2019-01-25 18:40:42 +03:00
rpassmore
f1c3fecfb2 Allow configuring IP address the DNS server binds to (#552)
Closes #550.
2019-01-19 04:41:43 +03:00
Eugene Bujak
79eff5f260 Merge pull request #133 in DNS/adguard-dns from fix/544 to master
* commit '8d5d37c28161a4c4720fa82311c7061095385acf':
  Removed extra key
  Only allow single click on buttons
2019-01-16 17:17:17 +03:00
Ildar Kamalov
8d5d37c281 Removed extra key 2019-01-16 15:16:34 +03:00
Ildar Kamalov
e1bb428a6a Only allow single click on buttons
Closes #544
2019-01-16 14:51:17 +03:00
Eugene Bujak
f1b6da93cf Merge pull request #132 in DNS/adguard-dns from fix/536 to master
* commit '61f4b6f1aee8581e3c659569e0629f1e7280e73e':
  Update ru translation
  Update translations
2019-01-15 16:45:16 +03:00
Ildar Kamalov
61f4b6f1ae Update ru translation
Closes #536
2019-01-15 15:36:55 +03:00
Ildar Kamalov
f678eaf9c0 Update translations
Closes #540
2019-01-15 15:35:29 +03:00
Hoàng Rio
607089cd25 Fix missing translate key when added new filter url 2019-01-15 14:22:05 +03:00
Hoàng Rio
df94d76a8b Add translate to Filter ReactTable 2019-01-15 14:22:05 +03:00
Eugene Bujak
8294bb1c7c Bump version to v0.92-hotfix2 2019-01-11 15:28:16 +03:00
Eugene Bujak
ec157ac4ea Merge pull request #131 in DNS/adguard-dns from fix/521 to master
* commit 'c4ba2849643b27a0b454fe83a4a87f7c46138038':
  fix tests
  Added TCPListenAddr
2019-01-09 12:50:55 +03:00
Andrey Meshkov
c4ba284964 fix tests 2019-01-05 22:24:07 +03:00
Andrey Meshkov
f3a97ed7ab Added TCPListenAddr 2019-01-05 22:15:20 +03:00
118 changed files with 10396 additions and 5882 deletions

8
.codecov.yml Normal file
View File

@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
target: 40%
threshold: null
patch: false
changes: false

8
.gitignore vendored
View File

@@ -2,15 +2,19 @@
/.vscode
/.idea
/AdGuardHome
/AdGuardHome.exe
/AdGuardHome.yaml
/AdGuardHome.log
/data/
/build/
/dist/
/client/node_modules/
/querylog.json
/querylog.json.1
/scripts/translations/node_modules
/scripts/translations/oneskyapp.json
/a_main-packr.go
# Test output
dnsfilter/dnsfilter.TestLotsOfRules*.pprof
tests/top-1m.csv
dnsfilter/tests/top-1m.csv
dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof

56
.golangci.yml Normal file
View File

@@ -0,0 +1,56 @@
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 2m
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
- ".*generated.*"
- dnsfilter/rule_to_regexp.go
# all available settings of specific linters
linters-settings:
errcheck:
# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
ignore: fmt:.*,net:SetReadDeadline,net/http:^Write
gocyclo:
min-complexity: 20
lll:
line-length: 200
linters:
enable-all: true
disable:
- interfacer
- gocritic
- scopelint
- gochecknoglobals
- gochecknoinits
- prealloc
- maligned
- goconst # disabled until it's possible to configure
fast: true
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
# structcheck cannot detect usages while they're there
- .parentalServer. is unused
- .safeBrowsingServer. is unused
# errcheck
- Error return value of .s.closeConn. is not checked
- Error return value of ..*.Shutdown.
# goconst
- string .forcesafesearch.google.com. has 3 occurrences

29
.gometalinter.json Normal file
View File

@@ -0,0 +1,29 @@
{
"Vendor": true,
"Test": true,
"Deadline": "2m",
"Sort": ["linter", "severity", "path", "line"],
"Exclude": [
".*generated.*",
"dnsfilter/rule_to_regexp.go"
],
"EnableGC": true,
"Linters": {
"nakedret": {
"Command": "nakedret",
"Pattern": "^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$"
}
},
"WarnUnmatchedDirective": true,
"EnableAll": true,
"DisableAll": false,
"Disable": [
"maligned",
"goconst",
"vetshadow"
],
"Cyclo": 20,
"LineLength": 200
}

View File

@@ -1,15 +1,9 @@
language: go
sudo: false
go:
- 1.11.x
- 1.x
cache:
directories:
- $HOME/.cache/go-build
- $HOME/gopath/pkg/mod
- $HOME/Library/Caches/go-build
os:
- linux
- osx
@@ -21,10 +15,67 @@ before_install:
install:
- npm --prefix client install
cache:
directories:
- $HOME/.cache/go-build
- $HOME/gopath/pkg/mod
- $HOME/Library/Caches/go-build
script:
- node -v
- npm -v
- go test ./...
# Run tests
- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
# Make
- make build/static/index.html
- make
after_success:
- bash <(curl -s https://codecov.io/bash)
matrix:
include:
# Release build configuration
- name: release
go:
- 1.11.x
os:
- linux
script:
- node -v
- npm -v
# Run tests just in case
- go test -race -v -bench=. ./...
# Prepare releases
- ./release.sh
- ls -l dist
deploy:
provider: releases
api_key: $GITHUB_TOKEN
file:
- dist/AdGuardHome_*
on:
repo: AdguardTeam/AdGuardHome
tags: true
draft: true
file_glob: true
skip_cleanup: true
- name: docker
if: type != pull_request AND (branch = master OR tag IS present)
go:
- 1.11.x
os:
- linux
services:
- docker
before_script:
- nvm install node
- npm install -g npm
script:
- docker login -u="$DOCKER_USER" -p="$DOCKER_PASSWORD"
- ./build_docker.sh
after_script:
- docker images

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM golang:alpine AS build
RUN apk add --update git make build-base npm && \
rm -rf /var/cache/apk/*
WORKDIR /src/AdGuardHome
COPY . /src/AdGuardHome
RUN make
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
COPY --from=build /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
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]

View File

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

View File

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

View File

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

16
Dockerfile.travis Normal file
View File

@@ -0,0 +1,16 @@
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
COPY ./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
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]

View File

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

150
README.md
View File

@@ -11,11 +11,21 @@
<a href="https://adguard.com/">AdGuard.com</a> |
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
<a href="https://reddit.com/r/Adguard">Reddit</a> |
<a href="https://twitter.com/AdGuard">Twitter</a>
<a href="https://twitter.com/AdGuard">Twitter</a> |
<a href="https://t.me/adguard_en">Telegram</a>
<br /><br />
<a href="https://travis-ci.org/AdguardTeam/AdGuardHome">
<img src="https://travis-ci.org/AdguardTeam/AdGuardHome.svg" alt="Build status" />
</a>
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage" />
</a>
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card" />
</a>
<a href="https://golangci.com/r/github.com/AdguardTeam/AdGuardHome">
<img src="https://golangci.com/badges/github.com/AdguardTeam/AdGuardHome.svg" alt="GolangCI" />
</a>
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
</a>
@@ -29,130 +39,31 @@
<hr />
# AdGuard Home
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
## How does AdGuard Home work?
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.
AdGuard Home operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
* [Getting Started](#getting-started)
* [How to build from source](#how-to-build)
* [Contributing](#contributing)
* [Reporting issues](#reporting-issues)
* [Acknowledgments](#acknowledgments)
## How is this different from public AdGuard DNS servers?
<a id="getting-started"></a>
## Getting Started
Running your own AdGuard Home server allows you to do much more than using a public DNS server.
Please read the [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started) article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
* Choose what exactly will the server block or not block;
* Monitor your network activity;
* Add your own custom filtering rules;
Alternatively, you can use our [official Docker image](https://hub.docker.com/r/adguard/adguardhome).
In the future, AdGuard Home is supposed to become more than just a DNS server.
### Guides
## Installation
### Mac
Download this file: [AdGuardHome_v0.92-hotfix1_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Windows 64-bit
Download this file: [AdGuardHome_v0.92-hotfix1_Windows.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_Windows.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Linux 64-bit Intel
Download this file: [AdGuardHome_v0.92-hotfix1_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Linux 32-bit Intel
Download this file: [AdGuardHome_v0.92-hotfix1_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Raspberry Pi (32-bit ARM)
Download this file: [AdGuardHome_v0.92-hotfix1_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to update
We have not yet implemented an auto-update of AdGuard Home, but it is planned for future versions: #448.
At the moment, the update procedure is manual:
1. Download the new AdGuard Home binary.
2. Replace the old file with the new one.
3. Restart AdGuard Home.
## How to run
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with `sudo` in terminal:
```bash
sudo ./AdGuardHome
```
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service.
### Running without superuser
You can run AdGuard Home without superuser privileges, but you need to either grant the binary a capability (on Linux) or instruct it to use a different port (all platforms).
#### Granting the CAP_NET_BIND_SERVICE capability (on Linux)
Note: using this method requires the `setcap` utility. You may need to install it using your Linux distribution's package manager.
To allow AdGuard Home running on Linux to listen on port 53 without superuser privileges, run:
```bash
sudo setcap CAP_NET_BIND_SERVICE=+eip ./AdGuardHome
```
Then run `./AdGuardHome` as a unprivileged user.
#### Changing the DNS listen port
To configure AdGuard Home to listen on a port that does not require superuser privileges, edit `AdGuardHome.yaml` and find these two lines:
```yaml
dns:
port: 53
```
You can change port 53 to anything above 1024 to avoid requiring superuser privileges.
If the file does not exist, create it in the same folder, type these two lines down and save.
### Additional configuration
Upon the first execution, a file named `AdGuardHome.yaml` will be created, with default values written in it. You can modify the file while your AdGuard Home service is not running. Otherwise, any changes to the file will be lost because the running program will overwrite them.
Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possible parameters that you can configure are listed below:
* `bind_host` — Web interface IP address to listen on.
* `bind_port` — Web interface IP port to listen on.
* `auth_name` — Web interface optional authorization username.
* `auth_pass` — Web interface optional authorization password.
* `dns` — DNS configuration section.
* `port` — DNS server port to listen on.
* `protection_enabled` — Whether any kind of filtering and protection should be done, when off it works as a plain dns forwarder.
* `filtering_enabled` — Filtering of DNS requests based on filter lists.
* `blocked_response_ttl` — For how many seconds the clients should cache a filtered response. Low values are useful on LAN if you change filters very often, high values are useful to increase performance and save traffic.
* `querylog_enabled` — Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistical purposes).
* `ratelimit` — DDoS protection, specifies in how many packets per second a client should receive. Anything above that is silently dropped. To disable set 0, default is 20. Safe to disable if DNS server is not available from internet.
* `ratelimit_whitelist` — If you want exclude some IP addresses from ratelimiting but keep ratelimiting on for others, put them here.
* `refuse_any` — Another DDoS protection mechanism. Requests of type ANY are rarely needed, so refusing to serve them mitigates against attackers trying to use your DNS as a reflection. Safe to disable if DNS server is not available from internet.
* `bootstrap_dns` — DNS server used for initial hostname resolution in case if upstream server name is a hostname.
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17 if enabled.
* `parental_enabled` — Parental control-based DNS requests filtering.
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible.
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing.
* `upstream_dns` — List of upstream DNS servers.
* `filters` — List of filters, each filter has the following values:
* `enabled` — Current filter's status (enabled/disabled).
* `url` — URL pointing to the filter contents (filtering rules).
* `name` — Name of the filter. If it's an adguard syntax filter it will get updated automatically, otherwise it stays unchanged.
* `last_updated` — Time when the filter was last updated from server.
* `ID` - filter ID (must be unique).
* `user_rules` — User-specified filtering rules.
Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration)
* [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
* [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="how-to-build"></a>
## How to build from source
### Prerequisites
@@ -160,7 +71,7 @@ Removing an entry from settings file will reset it to the default value. Deletin
You will need:
* [go](https://golang.org/dl/) v1.11 or later.
* [node.js](https://nodejs.org/en/download/)
* [node.js](https://nodejs.org/en/download/) v10 or later.
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
@@ -178,6 +89,7 @@ cd AdGuardHome
make
```
<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
@@ -216,10 +128,12 @@ node upload.js
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
This software wouldn't have been possible without:
@@ -229,6 +143,8 @@ This software wouldn't have been possible without:
* [gcache](https://github.com/bluele/gcache)
* [miekg's dns](https://github.com/miekg/dns)
* [go-yaml](https://github.com/go-yaml/yaml)
* [service](https://godoc.org/github.com/kardianos/service)
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
* [Node.js](https://nodejs.org/) and it's libraries:
* [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler)

588
app.go
View File

@@ -1,163 +1,50 @@
package main
import (
"bufio"
"crypto/tls"
"fmt"
stdlog "log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"sync"
"syscall"
"time"
"github.com/gobuffalo/packr"
"github.com/hmage/golibs/log"
"golang.org/x/crypto/ssh/terminal"
)
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
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
}
const (
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
configSyslog = "syslog"
)
// main is the entry point
func main() {
log.Printf("AdGuard Home web interface backend, version %s\n", VersionString)
box := packr.NewBox("build/static")
{
executable, err := os.Executable()
if err != nil {
panic(err)
}
executableName := filepath.Base(executable)
if executableName == "AdGuardHome" {
// Binary build
config.ourBinaryDir = filepath.Dir(executable)
} else {
// Most likely we're debugging -- using current working directory in this case
workDir, _ := os.Getwd()
config.ourBinaryDir = workDir
}
log.Printf("Current working directory is %s", config.ourBinaryDir)
}
// config can be specified, which reads options from there, but other command line flags have to override config values
// therefore, we must do it manually instead of using a lib
{
var printHelp func()
var configFilename *string
var bindHost *string
var bindPort *int
var opts = []struct {
longName string
shortName string
description string
callbackWithValue func(value string)
callbackNoValue func()
}{
{"config", "c", "path to config file", func(value string) { configFilename = &value }, nil},
{"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }, nil},
{"port", "p", "port to serve HTTP pages on", func(value string) {
v, err := strconv.Atoi(value)
if err != nil {
panic("Got port that is not a number")
}
bindPort = &v
}, nil},
{"verbose", "v", "enable verbose output", nil, func() { log.Verbose = true }},
{"help", "h", "print this help", nil, func() { printHelp(); os.Exit(64) }},
}
printHelp = func() {
fmt.Printf("Usage:\n\n")
fmt.Printf("%s [options]\n\n", os.Args[0])
fmt.Printf("Options:\n")
for _, opt := range opts {
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
}
}
for i := 1; i < len(os.Args); i++ {
v := os.Args[i]
knownParam := false
for _, opt := range opts {
if v == "--"+opt.longName || v == "-"+opt.shortName {
if opt.callbackWithValue != nil {
if i+1 > len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
os.Exit(64)
}
i++
opt.callbackWithValue(os.Args[i])
} else if opt.callbackNoValue != nil {
opt.callbackNoValue()
}
knownParam = true
break
}
}
if !knownParam {
log.Printf("ERROR: unknown option %v\n", v)
printHelp()
os.Exit(64)
}
}
if configFilename != nil {
config.ourConfigFilename = *configFilename
}
args := loadOptions()
err := askUsernamePasswordIfPossible()
if err != nil {
log.Fatal(err)
}
// Do the upgrade if necessary
err = upgradeConfig()
if err != nil {
log.Fatal(err)
}
// parse from config file
err = parseConfig()
if err != nil {
log.Fatal(err)
}
// override bind host/port from the console
if bindHost != nil {
config.BindHost = *bindHost
}
if bindPort != nil {
config.BindPort = *bindPort
}
if args.serviceControlAction != "" {
handleServiceControlAction(args.serviceControlAction)
return
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
err := filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
// clear LastUpdated so it gets fetched right away
}
if len(filter.Rules) == 0 {
filter.LastUpdated = time.Time{}
}
}
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNeccessary(false)
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}()
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
@@ -166,124 +53,375 @@ func main() {
os.Exit(0)
}()
// Save the updated config
err := config.write()
// run the protection
run(args)
}
// run initializes configuration and runs the AdGuard Home
// run is a blocking method and it won't exit until the service is stopped!
func run(args options) {
// config file path can be overridden by command-line arguments:
if args.configFilename != "" {
config.ourConfigFilename = args.configFilename
}
// configure working dir and config path
initWorkingDir(args)
// configure log level and output
configureLogger(args)
// print the first message after logger is configured
log.Printf("AdGuard Home, version %s\n", VersionString)
log.Tracef("Current working directory is %s", config.ourWorkingDir)
if args.runningAsService {
log.Printf("AdGuard Home is running as a service")
}
config.firstRun = detectFirstRun()
// Do the upgrade if necessary
err := upgradeConfig()
if err != nil {
log.Fatal(err)
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
// parse from config file
err = parseConfig()
if err != nil {
log.Fatal(err)
}
// override bind host/port from the console
if args.bindHost != "" {
config.BindHost = args.bindHost
}
if args.bindPort != 0 {
config.BindPort = args.bindPort
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
err = filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
// clear LastUpdated so it gets fetched right away
}
if len(filter.Rules) == 0 {
filter.LastUpdated = time.Time{}
}
}
// Save the updated config
err = config.write()
if err != nil {
log.Fatal(err)
}
// Init the DNS server instance before registering HTTP handlers
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
initDNSServer(dnsBaseDir)
if !config.firstRun {
err = startDNSServer()
if err != nil {
log.Fatal(err)
}
err = startDHCPServer()
if err != nil {
log.Fatal(err)
}
}
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNecessary(false)
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}()
// Schedule automatic filters updates
go periodicallyRefreshFilters()
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
// 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))))
registerControlHandlers()
err = startDNSServer()
if err != nil {
log.Fatal(err)
// add handlers for /install paths, we only need them when we're not configured yet
if config.firstRun {
log.Printf("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
registerInstallHandlers()
}
err = startDHCPServer()
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
// for https, we have a separate goroutine loop
go func() {
for { // this is an endless loop
httpsServer.cond.L.Lock()
// this mechanism doesn't let us through until all conditions are ment
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
httpsServer.cond.Wait()
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
// validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(config.TLS)
if !data.usable {
log.Fatal(data.WarningValidation)
os.Exit(1)
}
config.Lock()
config.TLS = data // update warnings
config.Unlock()
// prepare certs for HTTPS server
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
certchain := make([]byte, len(config.TLS.CertificateChain))
copy(certchain, []byte(config.TLS.CertificateChain))
privatekey := make([]byte, len(config.TLS.PrivateKey))
copy(privatekey, []byte(config.TLS.PrivateKey))
cert, err := tls.X509KeyPair(certchain, privatekey)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
httpsServer.cond.L.Unlock()
// prepare HTTPS server
httpsServer.server = &http.Server{
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
printHTTPAddresses("https")
err = httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed {
log.Fatal(err)
os.Exit(1)
}
}
}()
// this loop is used as an ability to change listening host and/or port
for {
printHTTPAddresses("http")
// we need to have new instance, because after Shutdown() the Server is not usable
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
httpServer = &http.Server{
Addr: address,
}
err := httpServer.ListenAndServe()
if err != http.ErrServerClosed {
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
}
}
// initWorkingDir initializes the ourWorkingDir
// if no command-line arguments specified, we use the directory where our binary file is located
func initWorkingDir(args options) {
exec, err := os.Executable()
if err != nil {
log.Fatal(err)
panic(err)
}
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
log.Fatal(http.ListenAndServe(address, nil))
if args.workDir != "" {
// If there is a custom config file, use it's directory as our working dir
config.ourWorkingDir = args.workDir
} else {
config.ourWorkingDir = filepath.Dir(exec)
}
}
// configureLogger configures logger level and output
func configureLogger(args options) {
ls := getLogSettings()
// command-line arguments can override config settings
if args.verbose {
ls.Verbose = true
}
if args.logFile != "" {
ls.LogFile = args.logFile
}
log.Verbose = ls.Verbose
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if nothing else is configured
// Otherwise, we'll simply loose the log output
ls.LogFile = configSyslog
}
if ls.LogFile == "" {
return
}
if ls.LogFile == configSyslog {
// Use syslog where it is possible and eventlog on Windows
err := configureSyslog()
if err != nil {
log.Fatalf("cannot initialize syslog: %s", err)
}
} else {
logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile)
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
if err != nil {
log.Fatalf("cannot create a log file: %s", err)
}
stdlog.SetOutput(file)
}
}
func cleanup() {
log.Printf("Stopping AdGuard Home")
err := stopDNSServer()
if err != nil {
log.Printf("Couldn't stop DNS server: %s", err)
}
err = stopDHCPServer()
if err != nil {
log.Printf("Couldn't stop DHCP server: %s", err)
}
}
func getInput() (string, error) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
text := scanner.Text()
err := scanner.Err()
return text, err
// command-line arguments
type options struct {
verbose bool // is verbose logging enabled
configFilename string // path to the config file
workDir string // path to the working directory where we will store the filters data and the querylog
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
// service control action (see service.ControlAction array + "status" command)
serviceControlAction string
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
}
func promptAndGet(prompt string) (string, error) {
for {
fmt.Print(prompt)
input, err := getInput()
// loadOptions reads command line arguments and initializes configuration
func loadOptions() options {
o := options{}
var printHelp func()
var opts = []struct {
longName string
shortName string
description string
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) {
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) {
o.serviceControlAction = value
}, nil},
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- 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() {
printHelp()
os.Exit(64)
}},
}
printHelp = func() {
fmt.Printf("Usage:\n\n")
fmt.Printf("%s [options]\n\n", os.Args[0])
fmt.Printf("Options:\n")
for _, opt := range opts {
if opt.shortName != "" {
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
} else {
fmt.Printf(" %-34s %s\n", "--"+opt.longName, opt.description)
}
}
}
for i := 1; i < len(os.Args); i++ {
v := os.Args[i]
knownParam := false
for _, opt := range opts {
if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
if opt.callbackWithValue != nil {
if i+1 >= len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
os.Exit(64)
}
i++
opt.callbackWithValue(os.Args[i])
} else if opt.callbackNoValue != nil {
opt.callbackNoValue()
}
knownParam = true
break
}
}
if !knownParam {
log.Printf("ERROR: unknown option %v\n", v)
printHelp()
os.Exit(64)
}
}
return o
}
// prints IP addresses which user can use to open the admin interface
// proto is either "http" or "https"
func printHTTPAddresses(proto string) {
var address string
if proto == "https" && config.TLS.ServerName != "" {
if config.TLS.PortHTTPS == 443 {
log.Printf("Go to https://%s", config.TLS.ServerName)
} else {
log.Printf("Go to https://%s:%d", config.TLS.ServerName, config.TLS.PortHTTPS)
}
} else if config.BindHost == "0.0.0.0" {
log.Println("AdGuard Home is available on the following addresses:")
ifaces, err := getValidNetInterfacesForWeb()
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
// That's weird, but we'll ignore it
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
return
}
if len(input) != 0 {
return input, nil
for _, iface := range ifaces {
address = net.JoinHostPort(iface.Addresses[0], strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
}
// try again
} else {
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
}
}
func promptAndGetPassword(prompt string) (string, error) {
for {
fmt.Print(prompt)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Print("\n")
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
}
if len(password) != 0 {
return string(password), nil
}
// try again
}
}
func askUsernamePasswordIfPossible() error {
configfile := config.ourConfigFilename
if !filepath.IsAbs(configfile) {
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
}
_, err := os.Stat(configfile)
if !os.IsNotExist(err) {
// do nothing, file exists
return nil
}
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
return nil // do nothing
}
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
return nil // do nothing
}
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
if err != nil {
return err
}
if yesno[0] != 'y' && yesno[0] != 'Y' {
return nil
}
username, err := promptAndGet("Please enter the username: ")
if err != nil {
return err
}
password, err := promptAndGetPassword("Please enter the password: ")
if err != nil {
return err
}
password2, err := promptAndGetPassword("Please enter password again: ")
if err != nil {
return err
}
if password2 != password {
fmt.Printf("Passwords do not match! Aborting\n")
os.Exit(1)
}
config.AuthName = username
config.AuthPass = password
return nil
}

74
build_docker.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -eE
set -o pipefail
set -x
DOCKERFILE="Dockerfile.travis"
IMAGE_NAME="adguard/adguardhome"
if [[ "${TRAVIS_BRANCH}" == "master" ]]
then
VERSION="edge"
else
VERSION=`git describe --abbrev=4 --dirty --always --tags`
fi
build_image() {
from="$(awk '$1 == toupper("FROM") { print $2 }' ${DOCKERFILE})"
# See https://hub.docker.com/r/multiarch/alpine/tags
case "${GOARCH}" in
arm64)
alpineArch='arm64-edge'
imageArch='arm64'
;;
arm)
alpineArch='armhf-edge'
imageArch='armhf'
;;
386)
alpineArch='i386-edge'
imageArch='i386'
;;
amd64)
alpineArch='amd64-edge'
;;
*)
alpineArch='amd64-edge'
;;
esac
if [[ "${GOOS}" == "linux" ]] && [[ "${GOARCH}" == "amd64" ]]
then
image="${IMAGE_NAME}:${VERSION}"
else
image="${IMAGE_NAME}:${imageArch}-${VERSION}"
fi
make cleanfast; CGO_DISABLED=1 make
docker pull "multiarch/alpine:${alpineArch}"
docker tag "multiarch/alpine:${alpineArch}" "$from"
docker build -t "${image}" -f ${DOCKERFILE} .
docker push ${image}
if [[ "${VERSION}" != "edge" ]]
then
latest=${image/$VERSION/latest}
docker tag "${image}" "${latest}"
docker push ${latest}
docker rmi ${latest}
fi
docker rmi "$from"
}
# prepare qemu
docker run --rm --privileged multiarch/qemu-user-static:register --reset
make clean
# Prepare releases
GOOS=linux GOARCH=amd64 build_image
GOOS=linux GOARCH=386 build_image
GOOS=linux GOARCH=arm GOARM=6 build_image
GOOS=linux GOARCH=arm64 GOARM=6 build_image

4
client/.eslintrc vendored
View File

@@ -45,9 +45,7 @@
}],
"class-methods-use-this": "off",
"no-shadow": "off",
"camelcase": ["error", {
"properties": "never"
}],
"camelcase": "off",
"no-console": ["warn", { "allow": ["warn", "error"] }],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"import/prefer-default-export": "off"

7158
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

11
client/package.json vendored
View File

@@ -16,7 +16,7 @@
"file-saver": "^1.3.8",
"i18next": "^12.0.0",
"i18next-browser-languagedetector": "^2.2.3",
"lodash": "^4.17.10",
"lodash": "^4.17.11",
"nanoid": "^1.2.3",
"prop-types": "^15.6.1",
"react": "^16.4.0",
@@ -33,14 +33,12 @@
"redux-actions": "^2.4.0",
"redux-form": "^7.4.2",
"redux-thunk": "^2.3.0",
"svg-url-loader": "^2.3.2",
"whatwg-fetch": "2.0.3"
"svg-url-loader": "^2.3.2"
},
"devDependencies": {
"autoprefixer": "^8.6.3",
"babel-core": "6.26.0",
"babel-eslint": "^8.2.3",
"babel-jest": "20.0.3",
"babel-loader": "7.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
@@ -60,7 +58,6 @@
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "1.1.5",
"html-webpack-plugin": "^3.2.0",
"jest": "20.0.4",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5",
@@ -68,12 +65,12 @@
"postcss-preset-env": "^5.1.0",
"postcss-svg": "^2.4.0",
"style-loader": "^0.21.0",
"stylelint": "9.2.1",
"stylelint": "^9.10.1",
"stylelint-webpack-plugin": "0.10.4",
"uglifyjs-webpack-plugin": "^1.2.7",
"url-loader": "^1.0.1",
"webpack": "3.8.1",
"webpack-dev-server": "2.9.4",
"webpack-dev-server": "^3.1.14",
"webpack-merge": "^4.1.3"
}
}

View File

@@ -1,16 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
<title>AdGuard Home</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
<title>AdGuard Home</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
<title>Setup AdGuard Home</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -1,4 +1,5 @@
{
"url_added_successfully": "Url added successfully",
"check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save config",
"enabled_dhcp": "DHCP server enabled",
@@ -8,7 +9,7 @@
"dhcp_enable": "Enable DHCP server",
"dhcp_disable": "Disable DHCP server",
"dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.",
"dhcp_found": "Found active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
"dhcp_found": "Some active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
"dhcp_leases": "DHCP leases",
"dhcp_leases_not_found": "No DHCP leases found",
"dhcp_config_saved": "Saved DHCP server config",
@@ -25,6 +26,9 @@
"dhcp_interface_select": "Select DHCP interface",
"dhcp_hardware_address": "Hardware address",
"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!",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
@@ -112,6 +116,7 @@
"example_comment": "! Here goes a comment",
"example_comment_meaning": "just a comment",
"example_comment_hash": "# Also a comment",
"example_regex_meaning": "block access to the domains matching the specified regular expression",
"example_upstream_regular": "regular DNS (over UDP)",
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
@@ -153,5 +158,93 @@
"category_label": "Category",
"rule_label": "Rule",
"filter_label": "Filter",
"unknown_filter": "Unknown filter {{filterId}}"
"unknown_filter": "Unknown filter {{filterId}}",
"install_welcome_title": "Welcome to AdGuard Home!",
"install_welcome_desc": "AdGuard Home is a network-wide ad-and-tracker blocking DNS server. Its purpose is to let you control your entire network and all your devices, and it does not require using a client-side program.",
"install_settings_title": "Admin Web Interface",
"install_settings_listen": "Listen interface",
"install_settings_port": "Port",
"install_settings_interface_link": "Your AdGuard Home admin web interface will be available on the following addresses:",
"form_error_port": "Enter valid port value",
"install_settings_dns": "DNS server",
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:",
"install_settings_all_interfaces": "All interfaces",
"install_auth_title": "Authentication",
"install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.",
"install_auth_username": "Username",
"install_auth_password": "Password",
"install_auth_confirm": "Confirm password",
"install_auth_username_enter": "Enter username",
"install_auth_password_enter": "Enter password",
"install_step": "Step",
"install_devices_title": "Configure your devices",
"install_devices_desc": "In order for AdGuard Home to start working, you need to configure your devices to use it.",
"install_submit_title": "Congratulations!",
"install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.",
"install_devices_router": "Router",
"install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.",
"install_devices_address": "AdGuard Home DNS server is listening to the following addresses",
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http:\/\/192.168.0.1\/ or http:\/\/192.168.1.1\/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer\/phone.",
"install_devices_router_list_2": "Find the DHCP\/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
"install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
"install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
"install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
"install_devices_windows_list_4": "Select your active connection, right-click on it and choose Properties.",
"install_devices_windows_list_5": "Find Internet Protocol Version 4 (TCP\/IP) in the list, select it and then click on Properties again.",
"install_devices_windows_list_6": "Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.",
"install_devices_macos_list_1": "Click on Apple icon and go to System Preferences.",
"install_devices_macos_list_2": "Click on Network.",
"install_devices_macos_list_3": "Select the first connection in your list and click Advanced.",
"install_devices_macos_list_4": "Select the DNS tab and enter your AdGuard Home server addresses.",
"install_devices_android_list_1": "From the Android Menu home screen, tap Settings.",
"install_devices_android_list_2": "Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).",
"install_devices_android_list_3": "Long press the network you're connected to, and tap Modify Network.",
"install_devices_android_list_4": "On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.",
"install_devices_android_list_5": "Change set DNS 1 and DNS 2 values to your AdGuard Home server addresses.",
"install_devices_ios_list_1": "From the home screen, tap Settings.",
"install_devices_ios_list_2": "Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).",
"install_devices_ios_list_3": "Tap on the name of the currently active network.",
"install_devices_ios_list_4": "In the DNS field enter your AdGuard Home server addresses.",
"get_started": "Get Started",
"next": "Next",
"open_dashboard": "Open Dashboard",
"install_saved": "Saved successfully",
"encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need yo enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy/paste your PEM-encoded cerificates here.",
"encryption_status": "Status",
"encryption_expire": "Expires",
"encryption_key": "Private key",
"encryption_key_input": "Copy/paste your PEM-encoded private key for your cerficate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid",
"encryption_chain_invalid": "Certificate chain is invalid",
"encryption_key_valid": "This is a valid {{type}} private key",
"encryption_key_invalid": "This is an invalid {{type}} private key",
"encryption_subject": "Subject",
"encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
"form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal",
"form_error_password": "Password mismatched",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info."
}

View File

@@ -3,7 +3,7 @@
"save_config": "Guardar config",
"enabled_dhcp": "Servidor DHCP habilitado",
"disabled_dhcp": "Servidor DHCP deshabilitado",
"dhcp_title": "Servidor DHCP",
"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_enable": "Habilitar servidor DHCP",
"dhcp_disable": "Deshabilitar el servidor DHCP",
@@ -25,6 +25,8 @@
"dhcp_interface_select": "Seleccione la interfaz DHCP",
"dhcp_hardware_address": "Direcci\u00f3n de hardware",
"dhcp_ip_addresses": "Direcciones IP",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expira",
"back": "Atr\u00e1s",
"dashboard": "Tablero de rendimiento",
"settings": "Ajustes",
@@ -44,7 +46,7 @@
"disabled_protection": "Protecci\u00f3n desactivada",
"refresh_statics": "Restablecer estad\u00edsticas",
"dns_query": "Consultas DNS",
"blocked_by": "Bloqueado por Filtros",
"blocked_by": "Bloqueado por filtros",
"stats_malware_phishing": "Malware\/phishing bloqueado",
"stats_adult": "Contenido para adultos bloqueado",
"stats_query_domain": "Dominios m\u00e1s solicitados",

View File

@@ -1,4 +1,33 @@
{
"url_added_successfully": "Url ajout\u00e9e",
"check_dhcp_servers": "Rechercher les serveurs DHCP",
"save_config": "Sauvegarder la configuration",
"enabled_dhcp": "Serveur DHCP activ\u00e9",
"disabled_dhcp": "Serveur DHCP d\u00e9sactiv\u00e9",
"dhcp_title": "Serveur DHCP (experimental !)",
"dhcp_description": "Si votre routeur ne fonctionne pas avec les r\u00e9glages DHCP, vous pouvez utiliser le serveur DHCP par d\u00e9faut d'AdGuard.",
"dhcp_enable": "Activer le serveur DHCP",
"dhcp_disable": "D\u00e9sactiver le serveur DHCP",
"dhcp_not_found": "Aucun serveur DHCP actif trouv\u00e9 sur le r\u00e9seau. Vous pouvez activer le serveur DHCP int\u00e9gr\u00e9.",
"dhcp_found": "Il y a plusieurs serveurs DHCP actifs sur le r\u00e9seau. Ce n'est pas prudent d'activer le serveur DHCP int\u00e9gr\u00e9 en ce moment.",
"dhcp_leases": "Locations des serveurs DHCP",
"dhcp_leases_not_found": "Aucune location des serveurs DHCP trouv\u00e9e",
"dhcp_config_saved": "La configuration du serveur DHCP est sauvegard\u00e9e",
"form_error_required": "Champ requis",
"form_error_ip_format": "Format IPv4 invalide",
"form_error_positive": "Doit \u00eatre sup\u00e9rieur \u00e0 0\u001c",
"dhcp_form_gateway_input": "IP de la passerelle",
"dhcp_form_subnet_input": "Masque de sous-r\u00e9seau",
"dhcp_form_range_title": "Rang\u00e9e des adresses IP",
"dhcp_form_range_start": "D\u00e9but de la rang\u00e9e",
"dhcp_form_range_end": "Fin de la rang\u00e9e",
"dhcp_form_lease_title": "P\u00e9riode de location du serveur DHCP (secondes)",
"dhcp_form_lease_input": "Dur\u00e9e de la location",
"dhcp_interface_select": "S\u00e9lectionner l'interface du serveur DHCP",
"dhcp_hardware_address": "Adresse de la machine",
"dhcp_ip_addresses": "Adresses IP",
"dhcp_table_hostname": "Nom de machine",
"dhcp_table_expires": "Expire le",
"back": "Retour",
"dashboard": "Tableau de bord",
"settings": "Param\u00e8tres",
@@ -89,6 +118,7 @@
"example_upstream_regular": "DNS classique (au-dessus de UDP)",
"example_upstream_dot": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-au-dessus-de-TLS<\/a> chiffr\u00e9",
"example_upstream_doh": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a> chiffr\u00e9",
"example_upstream_sdns": "vous pouvez utiliser <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> pour <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ou les resolveurs <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a>",
"example_upstream_tcp": "DNS classique (au-dessus de TCP)",
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",
@@ -125,5 +155,6 @@
"found_in_known_domain_db": "Trouv\u00e9 dans la base de donn\u00e9es des domaines connus",
"category_label": "Cat\u00e9gorie",
"rule_label": "R\u00e8gle",
"filter_label": "Filtre"
"filter_label": "Filtre",
"unknown_filter": "Filtre inconnu {{filterId}}"
}

View File

@@ -3,12 +3,12 @@
"save_config": "\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3059\u308b",
"enabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"dhcp_title": "DHCP\u30b5\u30fc\u30d0",
"dhcp_description": "\u3042\u306a\u305f\u306e\u30eb\u30fc\u30bf\u304cDHCP\u306e\u8a2d\u5b9a\u3092\u63d0\u4f9b\u3057\u3066\u3044\u306a\u3044\u306e\u306a\u3089\u3001AdGuard\u306b\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u5229\u7528\u3067\u304d\u307e\u3059\u3002",
"dhcp_title": "DHCP\u30b5\u30fc\u30d0\uff08\u5b9f\u9a13\u7684\uff01\uff09",
"dhcp_description": "\u3042\u306a\u305f\u306e\u30eb\u30fc\u30bf\u304cDHCP\u306e\u8a2d\u5b9a\u3092\u63d0\u4f9b\u3057\u3066\u3044\u306a\u3044\u306e\u306a\u3089\u3001AdGuard\u306b\u5185\u8535\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u5229\u7528\u3067\u304d\u307e\u3059\u3002",
"dhcp_enable": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b",
"dhcp_disable": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3059\u308b",
"dhcp_not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u5b89\u5168\u3067\u3059\u3002",
"dhcp_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u6d3b\u52d5\u4e2d\u306eDHCP\u30b5\u30fc\u30d0\u3092\u898b\u3064\u3051\u307e\u3057\u305f\u3002\u5185\u81d3\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002",
"dhcp_not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u5185\u8535\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u5b89\u5168\u3067\u3059\u3002",
"dhcp_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u898b\u3064\u3051\u307e\u3057\u305f\u3002\u5185\u81d3\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002",
"dhcp_leases": "DHCP\u5272\u5f53",
"dhcp_leases_not_found": "DHCP\u5272\u5f53\u306f\u3042\u308a\u307e\u305b\u3093",
"dhcp_config_saved": "DHCP\u30b5\u30fc\u30d0\u306e\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3057\u305f",
@@ -25,6 +25,8 @@
"dhcp_interface_select": "DHCP\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9\u306e\u9078\u629e",
"dhcp_hardware_address": "MAC\u30a2\u30c9\u30ec\u30b9",
"dhcp_ip_addresses": "IP\u30a2\u30c9\u30ec\u30b9",
"dhcp_table_hostname": "\u30db\u30b9\u30c8\u540d",
"dhcp_table_expires": "\u6709\u52b9\u671f\u9650",
"back": "\u623b\u308b",
"dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9",
"settings": "\u8a2d\u5b9a",

View File

@@ -1,14 +1,15 @@
{
"url_added_successfully": "Url adicionada com sucesso",
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
"save_config": "Salvar configura\u00e7\u00e3o",
"enabled_dhcp": "Servidor DHCP ativado",
"disabled_dhcp": "Servidor DHCP desativado",
"dhcp_title": "Servidor DHCP",
"dhcp_title": "Servidor DHCP (experimental)",
"dhcp_description": "Se o seu roteador n\u00e3o fornecer configura\u00e7\u00f5es de DHCP, voc\u00ea poder\u00e1 usar o servidor DHCP integrado do AdGuard.",
"dhcp_enable": "Ativar servidor DHCP",
"dhcp_disable": "Desativar servidor DHCP",
"dhcp_not_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. \u00c9 seguro ativar o servidor DHCP integrado.",
"dhcp_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. N\u00e3o \u00e9 seguro ativar o servidor DHCP integrado.",
"dhcp_found": "Foram encontrados servidores DHCP ativos na rede. N\u00e3o \u00e9 seguro ativar o servidor DHCP integrado.",
"dhcp_leases": "Concess\u00f5es DHCP",
"dhcp_leases_not_found": "Nenhuma concess\u00e3o DHCP encontrada",
"dhcp_config_saved": "Salvar configura\u00e7\u00f5es do servidor DHCP",
@@ -25,6 +26,8 @@
"dhcp_interface_select": "Selecione a interface DHCP",
"dhcp_hardware_address": "Endere\u00e7o de hardware",
"dhcp_ip_addresses": "Endere\u00e7o de IP",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expira",
"back": "Voltar",
"dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es",

View File

@@ -1,4 +1,32 @@
{
"check_dhcp_servers": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b",
"save_config": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e",
"enabled_dhcp": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d",
"disabled_dhcp": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d",
"dhcp_title": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 (\u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439!)",
"dhcp_description": "\u0415\u0441\u043b\u0438 \u0432\u0430\u0448 \u0440\u043e\u0443\u0442\u0435\u0440 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 DHCP, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 AdGuard.",
"dhcp_enable": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440",
"dhcp_disable": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440",
"dhcp_not_found": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0435 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0432 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 DHCP.",
"dhcp_found": "\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0435 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438. \u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e.",
"dhcp_leases": "\u0410\u0440\u0435\u043d\u0434\u0430 DHCP",
"dhcp_leases_not_found": "\u0410\u0440\u0435\u043d\u0434\u0430 DHCP \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430",
"dhcp_config_saved": "\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430",
"form_error_required": "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435",
"form_error_ip_format": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 IPv4",
"form_error_positive": "\u0414\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 0",
"dhcp_form_gateway_input": "IP-\u0430\u0434\u0440\u0435\u0441 \u0448\u043b\u044e\u0437\u0430",
"dhcp_form_subnet_input": "\u041c\u0430\u0441\u043a\u0430 \u043f\u043e\u0434\u0441\u0435\u0442\u0438",
"dhcp_form_range_title": "\u0414\u0438\u0430\u043f\u0430\u0437\u043e\u043d IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432",
"dhcp_form_range_start": "\u041d\u0430\u0447\u0430\u043b\u043e \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430",
"dhcp_form_range_end": "\u041a\u043e\u043d\u0435\u0446 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430",
"dhcp_form_lease_title": "\u0412\u0440\u0435\u043c\u044f \u0430\u0440\u0435\u043d\u0434\u044b DHCP (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
"dhcp_form_lease_input": "\u0421\u0440\u043e\u043a \u0430\u0440\u0435\u043d\u0434\u044b",
"dhcp_interface_select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 DHCP",
"dhcp_hardware_address": "\u0410\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441",
"dhcp_ip_addresses": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
"dhcp_table_hostname": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430",
"dhcp_table_expires": "\u0418\u0441\u0442\u0435\u043a\u0430\u0435\u0442",
"back": "\u041d\u0430\u0437\u0430\u0434",
"dashboard": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
@@ -18,7 +46,7 @@
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u044b\u043a\u043b.",
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443",
"dns_query": "DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b",
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0424\u0438\u043b\u044c\u0442\u0440\u044b",
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c\u0438",
"stats_malware_phishing": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u044b\u0435 \u0438 \u0444\u0438\u0448\u0438\u043d\u0433\u043e\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b",
"stats_adult": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \"\u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0435\" \u0441\u0430\u0439\u0442\u044b",
"stats_query_domain": "\u0427\u0430\u0441\u0442\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
@@ -89,6 +117,7 @@
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>",
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>",
"example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u0434\u043b\u044f <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u0438\u043b\u0438 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432",
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
@@ -125,5 +154,6 @@
"found_in_known_domain_db": "\u041d\u0430\u0439\u0434\u0435\u043d \u0432 \u0431\u0430\u0437\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.",
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440"
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440",
"unknown_filter": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0444\u0438\u043b\u044c\u0442\u0440 {{filterId}}"
}

View File

@@ -1,13 +1,15 @@
{
"refresh_status": "Uppdatera status",
"url_added_successfully": "URL tillagd utan fel",
"check_dhcp_servers": "Letar efter DHCP-servrar",
"save_config": "Spara inst\u00e4llningar",
"enabled_dhcp": "DHCP-server aktiverad",
"disabled_dhcp": "Dhcp-server avaktiverad",
"dhcp_title": "DHCP-server",
"dhcp_title": "DHCP-server (experimentell)",
"dhcp_description": "Om din router inte har inst\u00e4llningar f\u00f6r DHCP kan du anv\u00e4nda AdGuards inbyggda server.",
"dhcp_enable": "Aktivera DHCP.-server",
"dhcp_disable": "Avaktivera DHCP-server",
"dhcp_not_found": "Ingen aktiv DHCP-server hittades i n\u00e4tverkat.",
"dhcp_found": "N\u00e5gra aktiva DHCP-servar uppt\u00e4cktes. Det \u00e4r inte s\u00e4kert att aktivera inbyggda DHCP-servrar.",
"dhcp_leases": "DHCP-lease",
"dhcp_leases_not_found": "Ingen DHCP-lease hittad",
"dhcp_config_saved": "Sparade inst\u00e4llningar f\u00f6r DHCP-servern",
@@ -21,6 +23,11 @@
"dhcp_form_range_end": "Gr\u00e4nsslut",
"dhcp_form_lease_title": "DHCP-leasetid (i sekunder)",
"dhcp_form_lease_input": "Leasetid",
"dhcp_interface_select": "V\u00e4lj DHCP-gr\u00e4nssnitt",
"dhcp_hardware_address": "H\u00e5rdvaruadress",
"dhcp_ip_addresses": "IP-adresser",
"dhcp_table_hostname": "V\u00e4rdnamn",
"dhcp_table_expires": "Utg\u00e5r",
"back": "Tiilbaka",
"dashboard": "Kontrollpanel",
"settings": "Inst\u00e4llningar",

View File

@@ -1,4 +1,30 @@
{
"check_dhcp_servers": "Ki\u1ec3m tra m\u00e1y ch\u1ee7 DHCP",
"save_config": "L\u01b0u thi\u1ebft l\u1eadp",
"enabled_dhcp": "M\u00e1y ch\u1ee7 DHCP \u0111\u00e3 k\u00edch ho\u1ea1t",
"disabled_dhcp": "M\u00e1y ch\u1ee7 DHCP \u0111\u00e3 t\u1eaft",
"dhcp_title": "M\u00e1y ch\u1ee7 DHCP (th\u1eed nghi\u1ec7m!)",
"dhcp_description": "N\u1ebfu b\u1ed9 \u0111\u1ecbnh tuy\u1ebfn kh\u00f4ng tr\u1ee3 c\u00e0i \u0111\u1eb7t DHCP, b\u1ea1n c\u00f3 th\u1ec3 d\u00f9ng m\u00e1y ch\u1ee7 DHCP d\u1ef1ng s\u1eb5n c\u1ee7a AdGuard",
"dhcp_enable": "B\u1eadt m\u00e1y ch\u1ee7 DHCP",
"dhcp_disable": "T\u1eaft m\u00e1y ch\u1ee7 DHCP",
"dhcp_not_found": "Kh\u00f4ng c\u00f3 m\u00e1y ch\u1ee7 DHCP n\u00e0o \u0111\u01b0\u1ee3c t\u00ecm th\u1ea5y trong m\u1ea1ng. C\u00f3 th\u1ec3 b\u1eadt m\u00e1y ch\u1ee7 DHCP m\u1ed9t c\u00e1ch an to\u00e0n",
"dhcp_found": "\u0110\u00e3 t\u00ecm th\u1ea5y m\u00e1y ch\u1ee7 DHCP trong m\u1ea1ng. C\u00f3 th\u1ec3 c\u00f3 r\u1ee7i ro n\u1ebfu k\u00edch ho\u1ea1t m\u00e1y ch\u1ee7 DHCP d\u1ef1ng s\u1eb5n",
"dhcp_leases": "DHCP leases",
"dhcp_leases_not_found": "No DHCP leases found",
"dhcp_config_saved": "Saved DHCP server config",
"form_error_required": "Required field",
"form_error_ip_format": "Invalid IPv4 format",
"form_error_positive": "Ph\u1ea3i l\u1edbn h\u01a1n 0",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Range of IP addresses",
"dhcp_form_range_start": "Range start",
"dhcp_form_range_end": "IP k\u1ebft th\u00fac",
"dhcp_form_lease_title": "DHCP lease time (in seconds)",
"dhcp_form_lease_input": "Lease duration",
"dhcp_interface_select": "Ch\u1ecdn m\u1ed9t card m\u1ea1ng",
"dhcp_hardware_address": "Hardware address",
"dhcp_ip_addresses": "IP addresses",
"back": "Quay l\u1ea1i",
"dashboard": "T\u1ed5ng quan",
"settings": "C\u00e0i \u0111\u1eb7t",
@@ -18,7 +44,7 @@
"disabled_protection": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7",
"refresh_statics": "L\u00e0m m\u1edbi th\u1ed1ng k\u00ea",
"dns_query": "Truy v\u1ea5n DNS",
"blocked_by": "Ch\u1eb7n b\u1edfi B\u1ed9 l\u1ecdc",
"blocked_by": "Ch\u1eb7n b\u1edfi b\u1ed9 l\u1ecdc",
"stats_malware_phishing": "M\u00e3 \u0111\u1ed9c\/l\u1eeba \u0111\u1ea3o \u0111\u00e3 ch\u1eb7n",
"stats_adult": "Website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
"stats_query_domain": "T\u00ean mi\u1ec1n truy v\u1ea5n nhi\u1ec1u",
@@ -87,8 +113,9 @@
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_upstream_regular": "DNS th\u00f4ng th\u01b0\u1eddng (d\u00f9ng UDP)",
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-d\u1ef1a te-TLS<\/a>",
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ho\u1eb7c<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> ",
"example_upstream_tcp": "DNS th\u00f4ng th\u01b0\u1eddng(d\u00f9ng TCP)",
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
@@ -103,7 +130,7 @@
"client_table_header": "Ng\u01b0\u1eddi d\u00f9ng cu\u1ed1i",
"empty_response_status": "R\u1ed7ng",
"show_all_filter_type": "Hi\u1ec7n t\u1ea5t c\u1ea3",
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 ch\u1eb7n",
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 l\u1ecdc",
"no_logs_found": "Kh\u00f4ng c\u00f3 l\u1ecbch s\u1eed truy v\u1ea5n",
"disabled_log_btn": "T\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
"download_log_file_btn": "T\u1ea3i t\u1eadp tin l\u1ecbch s\u1eed truy v\u1ea5n",
@@ -125,5 +152,7 @@
"found_in_known_domain_db": "T\u00ecm th\u1ea5y trong c\u01a1 s\u1edf d\u1eef li\u1ec7u t\u00ean mi\u1ec1n",
"category_label": "Th\u1ec3 lo\u1ea1i",
"rule_label": "Quy t\u1eafc",
"filter_label": "B\u1ed9 l\u1ecdc"
"filter_label": "B\u1ed9 l\u1ecdc",
"url_added_successfully": "Th\u00eam b\u1ed9 l\u1ecdc th\u00e0nh c\u00f4ng",
"unknown_filter": "B\u1ed9 l\u1ecdc kh\u00f4ng r\u00f5 {{filterId}}"
}

View File

@@ -1,14 +1,15 @@
{
"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\u5df2\u88ab\u555f\u7528",
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u5df2\u88ab\u7981\u7528",
"dhcp_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"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_enable": "\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_disable": "\u7981\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_not_found": "\u65bc\u7db2\u8def\u4e0a\u7121\u5df2\u767c\u73fe\u4e4b\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
"dhcp_found": "\u65bc\u7db2\u8def\u4e0a\u5df2\u767c\u73fe\u4e4b\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_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_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_leases_not_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_config_saved": "\u5df2\u5132\u5b58\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u914d\u7f6e",
@@ -17,7 +18,7 @@
"form_error_positive": "\u5fc5\u9808\u5927\u65bc0",
"dhcp_form_gateway_input": "\u9598\u9053 IP",
"dhcp_form_subnet_input": "\u5b50\u7db2\u8def\u906e\u7f69",
"dhcp_form_range_title": "IP\u4f4d\u5740\u7bc4\u570d",
"dhcp_form_range_title": "IP \u4f4d\u5740\u7bc4\u570d",
"dhcp_form_range_start": "\u7bc4\u570d\u958b\u59cb",
"dhcp_form_range_end": "\u7bc4\u570d\u7d50\u675f",
"dhcp_form_lease_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3\u6642\u9593\uff08\u4ee5\u79d2\u6578\uff09",
@@ -25,6 +26,9 @@
"dhcp_interface_select": "\u9078\u64c7\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4ecb\u9762",
"dhcp_hardware_address": "\u786c\u9ad4\u4f4d\u5740",
"dhcp_ip_addresses": "IP \u4f4d\u5740",
"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",
"back": "\u8fd4\u56de",
"dashboard": "\u5100\u8868\u677f",
"settings": "\u8a2d\u5b9a",
@@ -33,8 +37,8 @@
"faq": "\u5e38\u898b\u554f\u7b54\u96c6",
"version": "\u7248\u672c",
"address": "\u4f4d\u5740",
"on": "\u958b\u555f",
"off": "\u95dc\u9589",
"on": "\u958b\u8457",
"off": "\u95dc\u8457",
"copyright": "\u7248\u6b0a",
"homepage": "\u9996\u9801",
"report_an_issue": "\u5831\u544a\u554f\u984c",
@@ -55,7 +59,7 @@
"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_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684DNS\u67e5\u8a62\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours": "\u5df2\u88ab\u5ee3\u544a\u5c01\u9396\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88abAdGuard\u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9\u4e4b\u6578\u91cf",
@@ -70,7 +74,7 @@
"use_adguard_parental": "\u4f7f\u7528AdGuard\u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
"use_adguard_parental_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
"enforce_safe_search": "\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b",
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4ee5\u4e0b\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"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",
"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",
@@ -112,24 +116,25 @@
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
"example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u914d\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>",
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u5c0d\u65bc <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u6216 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u89e3\u6790\u5668\u4e4b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS \u6233\u8a18<\/a>",
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u6216 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u89e3\u6790\u5668\u4e4b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS \u6233\u8a18<\/a>",
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684",
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"dns_test_ok_toast": "\u660e\u78ba\u6307\u5b9a\u7684DNS\u4f3a\u670d\u5668\u6b63\u78ba\u5730\u904b\u4f5c\u4e2d",
"dns_test_ok_toast": "\u5df2\u660e\u78ba\u6307\u5b9a\u7684DNS\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",
"time_table_header": "\u6642\u9593",
"domain_name_table_header": "\u57df\u540d",
"type_table_header": "\u985e\u578b",
"response_table_header": "\u53cd\u61c9",
"response_table_header": "\u56de\u61c9",
"client_table_header": "\u7528\u6236\u7aef",
"empty_response_status": "\u7a7a\u767d\u7684",
"show_all_filter_type": "\u986f\u793a\u6240\u6709",
"show_all_filter_type": "\u986f\u793a\u5168\u90e8",
"show_filtered_type": "\u986f\u793a\u5df2\u904e\u6ffe\u7684",
"no_logs_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u8a18\u9304",
"disabled_log_btn": "\u7981\u7528\u8a18\u9304",
@@ -144,14 +149,66 @@
"of_table_footer_text": "\u4e4b",
"rows_table_footer_text": "\u5217",
"updated_custom_filtering_toast": "\u5df2\u66f4\u65b0\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5df2\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u5df2\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u52a0\u5165",
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u555f\u7528",
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u88ab\u52a0\u81f3\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d",
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u555f\u7528",
"source_label": "\u4f86\u6e90",
"found_in_known_domain_db": "\u5728\u5df2\u77e5\u7684\u57df\u540d\u8cc7\u6599\u5eab\u4e2d\u88ab\u767c\u73fe\u3002",
"category_label": "\u985e\u5225",
"rule_label": "\u898f\u5247",
"filter_label": "\u904e\u6ffe\u5668",
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}"
"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_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",
"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_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_username": "\u7528\u6236\u540d",
"install_auth_password": "\u5bc6\u78bc",
"install_auth_confirm": "\u78ba\u8a8d\u5bc6\u78bc",
"install_auth_username_enter": "\u8f38\u5165\u7528\u6236\u540d",
"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_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_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_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_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_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_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_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_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",
"get_started": "\u958b\u59cb\u5427",
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc"
}

View File

@@ -0,0 +1,73 @@
import { createAction } from 'redux-actions';
import Api from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { redirectToCurrentProtocol } from '../helpers/helpers';
const apiClient = new Api();
export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');
export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');
export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');
export const getTlsStatus = () => async (dispatch) => {
dispatch(getTlsStatusRequest());
try {
const status = await apiClient.getTlsStatus();
status.certificate_chain = atob(status.certificate_chain);
status.private_key = atob(status.private_key);
dispatch(getTlsStatusSuccess(status));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTlsStatusFailure());
}
};
export const setTlsConfigRequest = createAction('SET_TLS_CONFIG_REQUEST');
export const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');
export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');
export const setTlsConfig = config => async (dispatch, getState) => {
dispatch(setTlsConfigRequest());
try {
const { httpPort } = getState().dashboard;
const values = { ...config };
values.certificate_chain = btoa(values.certificate_chain);
values.private_key = btoa(values.private_key);
values.port_https = values.port_https || 0;
values.port_dns_over_tls = values.port_dns_over_tls || 0;
const response = await apiClient.setTlsConfig(values);
response.certificate_chain = atob(response.certificate_chain);
response.private_key = atob(response.private_key);
dispatch(setTlsConfigSuccess(response));
dispatch(addSuccessToast('encryption_config_saved'));
redirectToCurrentProtocol(response, httpPort);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setTlsConfigFailure());
}
};
export const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUEST');
export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');
export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');
export const validateTlsConfig = config => async (dispatch) => {
dispatch(validateTlsConfigRequest());
try {
const values = { ...config };
values.certificate_chain = btoa(values.certificate_chain);
values.private_key = btoa(values.private_key);
values.port_https = values.port_https || 0;
values.port_dns_over_tls = values.port_dns_over_tls || 0;
const response = await apiClient.validateTlsConfig(values);
response.certificate_chain = atob(response.certificate_chain);
response.private_key = atob(response.private_key);
dispatch(validateTlsConfigSuccess(response));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(validateTlsConfigFailure());
}
};

View File

@@ -4,6 +4,7 @@ import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import { SETTINGS_NAMES } from '../helpers/constants';
import Api from '../api/Api';
const apiClient = new Api();
@@ -18,9 +19,8 @@ export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
export const toggleSetting = (settingKey, status) => async (dispatch) => {
let successMessage = '';
try {
// TODO move setting keys to constants
switch (settingKey) {
case 'filtering':
case SETTINGS_NAMES.filtering:
if (status) {
successMessage = 'disabled_filtering_toast';
await apiClient.disableFiltering();
@@ -30,7 +30,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safebrowsing':
case SETTINGS_NAMES.safebrowsing:
if (status) {
successMessage = 'disabled_safe_browsing_toast';
await apiClient.disableSafebrowsing();
@@ -40,7 +40,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'parental':
case SETTINGS_NAMES.parental:
if (status) {
successMessage = 'disabled_parental_toast';
await apiClient.disableParentalControl();
@@ -50,7 +50,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safesearch':
case SETTINGS_NAMES.safesearch:
if (status) {
successMessage = 'disabled_safe_search_toast';
await apiClient.disableSafesearch();
@@ -352,11 +352,11 @@ export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersRequest);
dispatch(refreshFiltersRequest());
dispatch(showLoading());
try {
const refreshText = await apiClient.refreshFilters();
dispatch(refreshFiltersSuccess);
dispatch(refreshFiltersSuccess());
if (refreshText.includes('OK')) {
if (refreshText.includes('OK 0')) {
@@ -434,7 +434,6 @@ export const downloadQueryLogRequest = createAction('DOWNLOAD_QUERY_LOG_REQUEST'
export const downloadQueryLogFailure = createAction('DOWNLOAD_QUERY_LOG_FAILURE');
export const downloadQueryLogSuccess = createAction('DOWNLOAD_QUERY_LOG_SUCCESS');
// TODO create some common flasher with all server errors
export const downloadQueryLog = () => async (dispatch) => {
let data;
dispatch(downloadQueryLogRequest());
@@ -573,36 +572,40 @@ export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
// TODO rewrite findActiveDhcp part
export const setDhcpConfig = config => async (dispatch) => {
export const setDhcpConfig = values => async (dispatch, getState) => {
const { config } = getState().dhcp;
const updatedConfig = { ...config, ...values };
dispatch(setDhcpConfigRequest());
try {
if (config.interface_name) {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
dispatch(findActiveDhcpSuccess(activeDhcp));
if (!activeDhcp.found) {
await apiClient.setDhcpConfig(config);
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'));
dispatch(setDhcpConfigSuccess());
dispatch(getDhcpStatus());
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}
} else {
await apiClient.setDhcpConfig(config);
dispatch(addSuccessToast('dhcp_config_saved'));
dispatch(setDhcpConfigSuccess());
dispatch(getDhcpStatus());
} 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());
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
};
@@ -615,11 +618,10 @@ export const toggleDhcp = config => async (dispatch) => {
dispatch(toggleDhcpRequest());
if (config.enabled) {
dispatch(addSuccessToast('disabled_dhcp'));
try {
await apiClient.setDhcpConfig({ ...config, enabled: false });
dispatch(toggleDhcpSuccess());
dispatch(getDhcpStatus());
dispatch(addSuccessToast('disabled_dhcp'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
@@ -634,12 +636,11 @@ export const toggleDhcp = config => async (dispatch) => {
try {
await apiClient.setDhcpConfig({ ...config, enabled: true });
dispatch(toggleDhcpSuccess());
dispatch(getDhcpStatus());
dispatch(addSuccessToast('enabled_dhcp'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
}
dispatch(addSuccessToast('enabled_dhcp'));
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}

View File

@@ -0,0 +1,46 @@
import { createAction } from 'redux-actions';
import Api from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
const apiClient = new Api();
export const nextStep = createAction('NEXT_STEP');
export const prevStep = createAction('PREV_STEP');
export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_REQUEST');
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
export const getDefaultAddresses = () => async (dispatch) => {
dispatch(getDefaultAddressesRequest());
try {
const addresses = await apiClient.getDefaultAddresses();
dispatch(getDefaultAddressesSuccess(addresses));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDefaultAddressesFailure());
}
};
export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
export const setAllSettings = values => async (dispatch) => {
dispatch(setAllSettingsRequest());
try {
const {
confirm_password,
...config
} = values;
await apiClient.setAllSettings(config);
dispatch(setAllSettingsSuccess());
dispatch(addSuccessToast('install_saved'));
dispatch(nextStep());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setAllSettingsFailure());
dispatch(prevStep());
}
};

View File

@@ -15,7 +15,11 @@ export default class Api {
return response.data;
} catch (error) {
console.error(error);
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
const errorPath = `${this.baseUrl}/${path}`;
if (error.response) {
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
}
throw new Error(`${errorPath} | ${error.message ? error.message : error}`);
}
}
@@ -336,4 +340,50 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
// Installation
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
getDefaultAddresses() {
const { path, method } = this.INSTALL_GET_ADDRESSES;
return this.makeRequest(path, method);
}
setAllSettings(config) {
const { path, method } = this.INSTALL_CONFIGURE;
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' };
TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };
getTlsStatus() {
const { path, method } = this.TLS_STATUS;
return this.makeRequest(path, method);
}
setTlsConfig(config) {
const { path, method } = this.TLS_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
validateTlsConfig(config) {
const { path, method } = this.TLS_VALIDATE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
}

View File

@@ -1,7 +1,7 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
}
.status {
@@ -19,8 +19,14 @@ body {
}
.loading-bar {
position: absolute;
position: fixed;
top: 0;
left: 0;
z-index: 103;
height: 3px;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}
.hidden {
display: none;
}

View File

@@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import LoadingBar from 'react-redux-loading-bar';
import 'react-table/react-table.css';
@@ -16,7 +17,8 @@ import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status';
import Update from '../ui/Update';
import UpdateTopline from '../ui/UpdateTopline';
import EncryptionTopline from '../ui/EncryptionTopline';
import i18n from '../../i18n';
class App extends Component {
@@ -50,7 +52,7 @@ class App extends Component {
}
render() {
const { dashboard } = this.props;
const { dashboard, encryption } = this.props;
const updateAvailable =
!dashboard.processingVersions &&
dashboard.isCoreRunning &&
@@ -60,11 +62,14 @@ class App extends Component {
<HashRouter hashType='noslash'>
<Fragment>
{updateAvailable &&
<Update
announcement={dashboard.announcement}
announcementUrl={dashboard.announcementUrl}
<UpdateTopline
url={dashboard.announcementUrl}
version={dashboard.version}
/>
}
{!encryption.processing &&
<EncryptionTopline notAfter={encryption.not_after} />
}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
@@ -100,6 +105,7 @@ App.propTypes = {
error: PropTypes.string,
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
};
export default App;
export default withNamespaces()(App);

View File

@@ -1,6 +1,5 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import 'whatwg-fetch';
import { Trans, withNamespaces } from 'react-i18next';
import Statistics from './Statistics';
@@ -25,12 +24,17 @@ class Dashboard extends Component {
}
getToggleFilteringButton = () => {
const { protectionEnabled } = this.props.dashboard;
const { protectionEnabled, processingProtection } = this.props.dashboard;
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
return (
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
<button
type="button"
className={`btn btn-sm mr-2 ${buttonClass}`}
onClick={() => this.props.toggleProtection(protectionEnabled)}
disabled={processingProtection}
>
<Trans>{buttonText}</Trans>
</button>
);
@@ -125,6 +129,7 @@ Dashboard.propTypes = {
isCoreRunning: PropTypes.bool,
getFiltering: PropTypes.func,
toggleProtection: PropTypes.func,
processingProtection: PropTypes.bool,
t: PropTypes.func,
};

View File

@@ -25,7 +25,7 @@ class UserRules extends Component {
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
<div className="card-actions">
<button
className="btn btn-success btn-standart"
className="btn btn-success btn-standard"
type="submit"
onClick={this.handleSubmit}
>
@@ -52,6 +52,9 @@ class UserRules extends Component {
<li>
<code>{ t('example_comment_hash') }</code> - { t('example_comment_meaning') }
</li>
<li>
<code>/REGEX/</code> - { t('example_regex_meaning') }
</li>
</ol>
</div>
</Card>

View File

@@ -68,7 +68,7 @@ class Filters extends Component {
render() {
const { t } = this.props;
const { filters, userRules } = this.props.filtering;
const { filters, userRules, processingRefreshFilters } = this.props.filtering;
return (
<div>
<PageTitle title={ t('filters') } />
@@ -84,12 +84,32 @@ class Filters extends Component {
columns={this.columns}
showPagination={true}
defaultPageSize={10}
minRows={4}
// Text
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('no_filters_added') }
minRows={4} // TODO find out what to show if rules.length is 0
/>
<div className="card-actions">
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}><Trans>add_filter_btn</Trans></button>
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}><Trans>check_updates_btn</Trans></button>
<button
className="btn btn-success btn-standard mr-2"
type="submit"
onClick={this.props.toggleFilteringModal}
>
<Trans>add_filter_btn</Trans>
</button>
<button
className="btn btn-primary btn-standard"
type="submit"
onClick={this.props.refreshFilters}
disabled={processingRefreshFilters}
>
<Trans>check_updates_btn</Trans>
</button>
</div>
</Card>
</div>
@@ -107,6 +127,7 @@ class Filters extends Component {
toggleModal={this.props.toggleFilteringModal}
addFilter={this.props.addFilter}
isFilterAdded={this.props.filtering.isFilterAdded}
processingAddFilter={this.props.filtering.processingAddFilter}
title={ t('new_filter_btn') }
inputDescription={ t('enter_valid_filter_url') }
/>
@@ -123,6 +144,8 @@ Filters.propTypes = {
filters: PropTypes.array,
isFilteringModalOpen: PropTypes.bool.isRequired,
isFilterAdded: PropTypes.bool,
processingAddFilter: PropTypes.bool,
processingRefreshFilters: PropTypes.bool,
}),
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,

View File

@@ -6,13 +6,12 @@ import { Trans, withNamespaces } from 'react-i18next';
import Menu from './Menu';
import Version from './Version';
import logo from './logo.svg';
import logo from '../ui/svg/logo.svg';
import './Header.css';
class Header extends Component {
state = {
isMenuOpen: false,
isDropdownOpen: false,
};
toggleMenuOpen = () => {
@@ -25,6 +24,7 @@ class Header extends Component {
render() {
const { dashboard } = this.props;
const { isMenuOpen } = this.state;
const badgeClass = classnames({
'badge dns-status': true,
'badge-success': dashboard.protectionEnabled,
@@ -52,7 +52,7 @@ class Header extends Component {
</div>
<Menu
location={this.props.location}
isMenuOpen={this.state.isMenuOpen}
isMenuOpen={isMenuOpen}
toggleMenuOpen={this.toggleMenuOpen}
closeMenu={this.closeMenu}
/>

View File

@@ -77,6 +77,7 @@ class Logs extends Component {
type="button"
className={`btn btn-sm ${buttonClass}`}
onClick={() => this.toggleBlocking(buttonType, domain)}
disabled={this.props.filtering.processingRules}
>
<Trans>{buttonText}</Trans>
</button>
@@ -269,7 +270,7 @@ class Logs extends Component {
saveAs(dataBlob, DOWNLOAD_LOG_FILENAME);
};
renderButtons(queryLogEnabled) {
renderButtons(queryLogEnabled, logStatusProcessing) {
if (queryLogEnabled) {
return (
<Fragment>
@@ -277,6 +278,7 @@ class Logs extends Component {
className="btn btn-gray btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing}
><Trans>disabled_log_btn</Trans></button>
<button
className="btn btn-primary btn-sm mr-2"
@@ -297,6 +299,7 @@ class Logs extends Component {
className="btn btn-success btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing}
><Trans>enabled_log_btn</Trans></button>
);
}
@@ -308,7 +311,7 @@ class Logs extends Component {
<Fragment>
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }>
<div className="page-title__actions">
{this.renderButtons(queryLogEnabled)}
{this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)}
</div>
</PageTitle>
<Card>
@@ -332,6 +335,8 @@ Logs.propTypes = {
userRules: PropTypes.string,
setRules: PropTypes.func,
addSuccessToast: PropTypes.func,
processingRules: PropTypes.bool,
logStatusProcessing: PropTypes.bool,
t: PropTypes.func,
};

View File

@@ -1,62 +1,25 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import { withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { R_IPV4 } from '../../../helpers/constants';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
const renderField = ({
input, className, placeholder, type, disabled, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
const Form = (props) => {
const {
t,
handleSubmit,
pristine,
submitting,
invalid,
processingConfig,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="gateway_ip"
@@ -67,7 +30,7 @@ const Form = (props) => {
validate={[ipv4, required]}
/>
</div>
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="subnet_mask"
@@ -80,7 +43,7 @@ const Form = (props) => {
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
@@ -107,7 +70,7 @@ const Form = (props) => {
</div>
</div>
</div>
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="lease_duration"
@@ -124,8 +87,8 @@ const Form = (props) => {
<button
type="submit"
className="btn btn-success btn-standart"
disabled={pristine || submitting}
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingConfig}
>
{t('save_config')}
</button>
@@ -135,11 +98,11 @@ const Form = (props) => {
Form.propTypes = {
handleSubmit: PropTypes.func,
pristine: PropTypes.bool,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
interfaces: PropTypes.object,
processing: PropTypes.bool,
initialValues: PropTypes.object,
processingConfig: PropTypes.bool,
t: PropTypes.func,
};

View File

@@ -63,7 +63,7 @@ let Interface = (props) => {
{!processing && interfaces &&
<div className="row">
<div className="col-sm-12 col-md-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_interface_select')}</label>
<Field
name="interface_name"

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { withNamespaces } from 'react-i18next';
import { Trans, withNamespaces } from 'react-i18next';
const columns = [{
Header: 'MAC',
@@ -10,10 +10,10 @@ const columns = [{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Hostname',
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',
}, {
Header: 'Expires',
Header: <Trans>dhcp_table_expires</Trans>,
accessor: 'expires',
}];

View File

@@ -13,17 +13,14 @@ class Dhcp extends Component {
this.props.setDhcpConfig(values);
};
handleFormChange = (value) => {
this.props.setDhcpConfig(value);
}
handleToggle = (config) => {
this.props.toggleDhcp(config);
this.props.findActiveDhcp(config.interface_name);
}
getToggleDhcpButton = () => {
const { config, active } = this.props.dhcp;
const {
config, active, processingDhcp, processingConfig,
} = this.props.dhcp;
const activeDhcpFound = active && active.found;
const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled') {
@@ -37,8 +34,9 @@ class Dhcp extends Component {
return (
<button
type="button"
className="btn btn-standart mr-2 btn-gray"
className="btn btn-standard mr-2 btn-gray"
onClick={() => this.props.toggleDhcp(config)}
disabled={processingDhcp || processingConfig}
>
<Trans>dhcp_disable</Trans>
</button>
@@ -48,9 +46,14 @@ class Dhcp extends Component {
return (
<button
type="button"
className="btn btn-standart mr-2 btn-success"
className="btn btn-standard mr-2 btn-success"
onClick={() => this.handleToggle(config)}
disabled={!filledConfig || activeDhcpFound}
disabled={
!filledConfig
|| activeDhcpFound
|| processingDhcp
|| processingConfig
}
>
<Trans>dhcp_enable</Trans>
</button>
@@ -63,14 +66,14 @@ class Dhcp extends Component {
if (active) {
if (active.error) {
return (
<div className="text-danger">
<div className="text-danger mb-2">
{active.error}
</div>
);
}
return (
<Fragment>
<div className="mb-2">
{active.found ? (
<div className="text-danger">
<Trans>dhcp_found</Trans>
@@ -80,7 +83,7 @@ class Dhcp extends Component {
<Trans>dhcp_not_found</Trans>
</div>
)}
</Fragment>
</div>
);
}
@@ -90,9 +93,14 @@ class Dhcp extends Component {
render() {
const { t, dhcp } = this.props;
const statusButtonClass = classnames({
'btn btn-primary btn-standart': true,
'btn btn-primary btn-standart btn-loading': dhcp.processingStatus,
'btn btn-primary btn-standard': true,
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
});
const {
enabled,
interface_name,
...values
} = dhcp.config;
return (
<Fragment>
@@ -101,17 +109,17 @@ class Dhcp extends Component {
{!dhcp.processing &&
<Fragment>
<Interface
onChange={this.handleFormChange}
initialValues={dhcp.config}
onChange={this.handleFormSubmit}
initialValues={{ interface_name }}
interfaces={dhcp.interfaces}
processing={dhcp.processingInterfaces}
enabled={dhcp.config.enabled}
/>
<Form
onSubmit={this.handleFormSubmit}
initialValues={dhcp.config}
initialValues={{ ...values }}
interfaces={dhcp.interfaces}
processing={dhcp.processingInterfaces}
processingConfig={dhcp.processingConfig}
/>
<hr/>
<div className="card-actions mb-3">
@@ -122,12 +130,18 @@ class Dhcp extends Component {
onClick={() =>
this.props.findActiveDhcp(dhcp.config.interface_name)
}
disabled={!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>

View File

@@ -0,0 +1,364 @@
import React, { Fragment } 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 format from 'date-fns/format';
import { renderField, renderSelectField, toNumber, port, isSafePort } from '../../../helpers/form';
import { EMPTY_DATE } from '../../../helpers/constants';
import i18n from '../../../i18n';
const validate = (values) => {
const errors = {};
if (values.port_dns_over_tls && values.port_https) {
if (values.port_dns_over_tls === values.port_https) {
errors.port_dns_over_tls = i18n.t('form_error_equal');
errors.port_https = i18n.t('form_error_equal');
}
}
return errors;
};
const clearFields = (change, setTlsConfig, t) => {
const fields = {
private_key: '',
certificate_chain: '',
port_https: 443,
port_dns_over_tls: 853,
server_name: '',
force_https: false,
enabled: false,
};
// eslint-disable-next-line no-alert
if (window.confirm(t('encryption_reset'))) {
Object.keys(fields).forEach(field => change(field, fields[field]));
setTlsConfig(fields);
}
};
let Form = (props) => {
const {
t,
handleSubmit,
handleChange,
isEnabled,
certificateChain,
privateKey,
change,
invalid,
submitting,
processingConfig,
processingValidate,
not_after,
valid_chain,
valid_key,
valid_cert,
dns_names,
key_type,
issuer,
subject,
warning_validation,
setTlsConfig,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('encryption_enable')}
onChange={handleChange}
/>
</div>
<div className="form__desc">
<Trans>encryption_enable_desc</Trans>
</div>
<hr/>
</div>
<div className="col-12">
<label className="form__label" htmlFor="server_name">
<Trans>encryption_server</Trans>
</label>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
id="server_name"
name="server_name"
component={renderField}
type="text"
className="form-control"
placeholder={t('encryption_server_enter')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_server_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
name="force_https"
type="checkbox"
component={renderSelectField}
placeholder={t('encryption_redirect')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_redirect_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_https">
<Trans>encryption_https</Trans>
</label>
<Field
id="port_https"
name="port_https"
component={renderField}
type="number"
className="form-control"
placeholder={t('encryption_https')}
validate={[port, isSafePort]}
normalize={toNumber}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_https_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_dns_over_tls">
<Trans>encryption_dot</Trans>
</label>
<Field
id="port_dns_over_tls"
name="port_dns_over_tls"
component={renderField}
type="number"
className="form-control"
placeholder={t('encryption_dot')}
validate={[port]}
normalize={toNumber}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_dot_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<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>]}
>
encryption_certificates_desc
</Trans>
</div>
<Field
id="certificate_chain"
name="certificate_chain"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('encryption_certificates_input')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__status">
{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 ?
<Trans>encryption_chain_valid</Trans>
: <Trans>encryption_chain_invalid</Trans>
}
</li>
{valid_cert &&
<Fragment>
{subject &&
<li>
<Trans>encryption_subject</Trans>:&nbsp;
{subject}
</li>
}
{issuer &&
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
}
{not_after && not_after !== EMPTY_DATE &&
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
</li>
}
{dns_names &&
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{dns_names}
</li>
}
</Fragment>
}
</ul>
</Fragment>
}
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label form__label--bold" htmlFor="private_key">
<Trans>encryption_key</Trans>
</label>
<Field
id="private_key"
name="private_key"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder="Copy/paste your PEM-encoded private key for your cerficate here."
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__status">
{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 ?
<Trans values={{ type: key_type }}>
encryption_key_valid
</Trans>
: <Trans values={{ type: key_type }}>
encryption_key_invalid
</Trans>
}
</li>
</ul>
</Fragment>
}
</div>
</div>
</div>
{warning_validation &&
<div className="col-12">
<p className="text-danger">
{warning_validation}
</p>
</div>
}
</div>
<div className="btn-list mt-2">
<button
type="submit"
className="btn btn-success btn-standart"
disabled={
invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
}
>
<Trans>save_config</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standart"
disabled={submitting || processingConfig}
onClick={() => clearFields(change, setTlsConfig, t)}
>
<Trans>reset_settings</Trans>
</button>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isEnabled: PropTypes.bool.isRequired,
certificateChain: PropTypes.string.isRequired,
privateKey: PropTypes.string.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
processingValidate: PropTypes.bool.isRequired,
status_key: PropTypes.string,
not_after: PropTypes.string,
warning_validation: PropTypes.string,
valid_chain: PropTypes.bool,
valid_key: PropTypes.bool,
valid_cert: PropTypes.bool,
dns_names: PropTypes.string,
key_type: PropTypes.string,
issuer: PropTypes.string,
subject: PropTypes.string,
t: PropTypes.func.isRequired,
setTlsConfig: PropTypes.func.isRequired,
};
const selector = formValueSelector('encryptionForm');
Form = connect((state) => {
const isEnabled = selector(state, 'enabled');
const certificateChain = selector(state, 'certificate_chain');
const privateKey = selector(state, 'private_key');
return {
isEnabled,
certificateChain,
privateKey,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'encryptionForm',
validate,
}),
])(Form);

View File

@@ -0,0 +1,72 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
import Form from './Form';
import Card from '../../ui/Card';
class Encryption extends Component {
componentDidMount() {
this.props.validateTlsConfig(this.props.encryption);
}
handleFormSubmit = (values) => {
this.props.setTlsConfig(values);
};
handleFormChange = debounce((values) => {
this.props.validateTlsConfig(values);
}, DEBOUNCE_TIMEOUT);
render() {
const { encryption, t } = this.props;
const {
enabled,
server_name,
force_https,
port_https,
port_dns_over_tls,
certificate_chain,
private_key,
} = encryption;
return (
<div className="encryption">
{encryption &&
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={{
enabled,
server_name,
force_https,
port_https,
port_dns_over_tls,
certificate_chain,
private_key,
}}
onSubmit={this.handleFormSubmit}
onChange={this.handleFormChange}
setTlsConfig={this.props.setTlsConfig}
{...this.props.encryption}
/>
</Card>
}
</div>
);
}
}
Encryption.propTypes = {
setTlsConfig: PropTypes.func.isRequired,
validateTlsConfig: PropTypes.func.isRequired,
encryption: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Encryption);

View File

@@ -7,11 +7,11 @@
margin-bottom: 0;
}
.form__group--dhcp:last-child {
margin-bottom: 15px;
.form__group--settings:last-child {
margin-bottom: 20px;
}
.btn-standart {
.btn-standard {
padding-left: 20px;
padding-right: 20px;
}
@@ -48,3 +48,31 @@
.dhcp {
min-height: 450px;
}
.form__desc {
margin-top: 10px;
font-size: 13px;
color: rgba(74, 74, 74, 0.7);
}
.form__desc--top {
margin: 0 0 8px;
}
.form__label--bold {
font-weight: 700;
}
.form__status {
margin-top: 10px;
font-size: 14px;
line-height: 1.7;
}
.encryption__list {
padding-left: 0;
}
.encryption__list li {
list-style: inside;
}

View File

@@ -21,8 +21,8 @@ class Upstream extends Component {
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standart mr-2': true,
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
@@ -49,7 +49,7 @@ class Upstream extends Component {
<Trans>test_upstream_btn</Trans>
</button>
<button
className="btn btn-success btn-standart"
className="btn btn-success btn-standard"
type="submit"
onClick={this.handleSubmit}
>

View File

@@ -3,6 +3,7 @@ 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';
@@ -37,6 +38,7 @@ class Settings extends Component {
this.props.initSettings(this.settings);
this.props.getDhcpStatus();
this.props.getDhcpInterfaces();
this.props.getTlsStatus();
}
handleUpstreamChange = (value) => {
@@ -95,6 +97,11 @@ class Settings extends Component {
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/>
<Encryption
encryption={this.props.encryption}
setTlsConfig={this.props.setTlsConfig}
validateTlsConfig={this.props.validateTlsConfig}
/>
<Dhcp
dhcp={this.props.dhcp}
toggleDhcp={this.props.toggleDhcp}

View File

@@ -22,6 +22,11 @@
font-weight: 600;
}
.checkbox--form .checkbox__label:before {
top: 2px;
margin-right: 10px;
}
.checkbox__label {
position: relative;
display: flex;
@@ -68,14 +73,19 @@
opacity: 0;
}
.checkbox__input:checked+.checkbox__label:before {
.checkbox__input:checked + .checkbox__label:before {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);
}
.checkbox__input:focus+.checkbox__label:before {
.checkbox__input:focus + .checkbox__label:before {
box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);
}
.checkbox__input:disabled + .checkbox__label {
opacity: 0.6;
cursor: default;
}
.checkbox__label-text {
max-width: 515px;
line-height: 1.5;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import isAfter from 'date-fns/is_after';
import addDays from 'date-fns/add_days';
import Topline from './Topline';
import { EMPTY_DATE } from '../../helpers/constants';
const EncryptionTopline = (props) => {
if (props.notAfter === EMPTY_DATE) {
return false;
}
const isAboutExpire = isAfter(addDays(Date.now(), 30), props.notAfter);
const isExpired = isAfter(Date.now(), props.notAfter);
if (isExpired) {
return (
<Topline type="danger">
<Trans components={[<a href="#settings" key="0">link</a>]}>
topline_expired_certificate
</Trans>
</Topline>
);
} else if (isAboutExpire) {
return (
<Topline type="warning">
<Trans components={[<a href="#settings" key="0">link</a>]}>
topline_expiring_certificate
</Trans>
</Topline>
);
}
return false;
};
EncryptionTopline.propTypes = {
notAfter: PropTypes.string.isRequired,
};
export default withNamespaces()(EncryptionTopline);

View File

@@ -23,7 +23,7 @@ class Footer extends Component {
<div className="footer__row">
<div className="footer__column">
<div className="footer__copyright">
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
<Trans>copyright</Trans> &copy; {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
</div>
</div>
<div className="footer__column">

File diff suppressed because one or more lines are too long

View File

@@ -55,6 +55,7 @@ class Modal extends Component {
isOpen,
title,
inputDescription,
processingAddFilter,
} = this.props;
const { isUrlValid, url, name } = this.state;
const inputUrlClass = classnames({
@@ -71,8 +72,8 @@ class Modal extends Component {
if (!this.props.isFilterAdded) {
return (
<React.Fragment>
<input type="text" className={inputNameClass} placeholder={ this.props.t('enter_name_hint') } onChange={this.handleNameChange} />
<input type="text" className={inputUrlClass} placeholder={ this.props.t('enter_url_hint') } onChange={this.handleUrlChange} />
<input type="text" className={inputNameClass} placeholder={this.props.t('enter_name_hint')} onChange={this.handleNameChange} />
<input type="text" className={inputUrlClass} placeholder={this.props.t('enter_url_hint')} onChange={this.handleUrlChange} />
{inputDescription &&
<div className="description">
{inputDescription}
@@ -82,7 +83,7 @@ class Modal extends Component {
}
return (
<div className="description">
<Trans>Url added successfully</Trans>
<Trans>url_added_successfully</Trans>
</div>
);
};
@@ -93,7 +94,7 @@ class Modal extends Component {
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0}
isOpen={ isOpen }
isOpen={isOpen}
onRequestClose={this.closeModal}
>
<div className="modal-content">
@@ -106,14 +107,26 @@ class Modal extends Component {
</button>
</div>
<div className="modal-body">
{ renderBody()}
{renderBody()}
</div>
{
!this.props.isFilterAdded &&
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={this.closeModal}><Trans>cancel_btn</Trans></button>
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}><Trans>add_filter_btn</Trans></button>
</div>
{!this.props.isFilterAdded &&
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={this.closeModal}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="button"
className="btn btn-success"
onClick={this.handleNext}
disabled={isValidForSubmit || processingAddFilter}
>
<Trans>add_filter_btn</Trans>
</button>
</div>
}
</div>
</ReactModal>
@@ -128,6 +141,7 @@ Modal.propTypes = {
inputDescription: PropTypes.string,
addFilter: PropTypes.func.isRequired,
isFilterAdded: PropTypes.bool,
processingAddFilter: PropTypes.bool,
t: PropTypes.func,
};

View File

@@ -0,0 +1,41 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
class Tab extends Component {
handleClick = () => {
this.props.onClick(this.props.label);
}
render() {
const {
activeTab,
label,
} = this.props;
const tabClass = classnames({
tab__control: true,
'tab__control--active': activeTab === label,
});
return (
<div
className={tabClass}
onClick={this.handleClick}
>
<svg className="tab__icon">
<use xlinkHref={`#${label.toLowerCase()}`} />
</svg>
{label}
</div>
);
}
}
Tab.propTypes = {
activeTab: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
export default Tab;

View File

@@ -3783,7 +3783,7 @@ tbody.collapse.show {
line-height: 1.5;
color: #495057;
vertical-align: middle;
background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'%3E%3Cpath fill='#999' d='M0 0L10 0L5 5L0 0'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
background-size: 8px 10px;
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;

View File

@@ -0,0 +1,51 @@
.tabs__controls {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding: 15px 0;
border-bottom: 1px solid #e8e8e8;
}
.tab__control {
display: flex;
flex-direction: column;
align-items: center;
min-width: 70px;
font-size: 13px;
color: #555555;
cursor: pointer;
opacity: 0.6;
}
.tab__control:hover,
.tab__control:focus {
opacity: 1;
}
.tab__control--active {
font-weight: 700;
color: #4a4a4a;
opacity: 1;
}
.tab__title {
margin-bottom: 10px;
font-size: 16px;
font-weight: 700;
}
.tab__icon {
width: 24px;
height: 24px;
margin-bottom: 6px;
fill: #4a4a4a;
}
.tab__text {
line-height: 1.7;
}
.tab__text li,
.tab__text p {
margin-bottom: 5px;
}

View File

@@ -0,0 +1,59 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Tab from './Tab';
import './Tabs.css';
class Tabs extends Component {
state = {
activeTab: this.props.children[0].props.label,
};
onClickTabControl = (tab) => {
this.setState({ activeTab: tab });
}
render() {
const {
props: {
children,
},
state: {
activeTab,
},
} = this;
return (
<div className="tabs">
<div className="tabs__controls">
{children.map((child) => {
const { label } = child.props;
return (
<Tab
key={label}
label={label}
activeTab={activeTab}
onClick={this.onClickTabControl}
/>
);
})}
</div>
<div className="tabs__content">
{children.map((child) => {
if (child.props.label !== activeTab) {
return false;
}
return child.props.children;
})}
</div>
</div>
);
}
}
Tabs.propTypes = {
children: PropTypes.array.isRequired,
};
export default Tabs;

View File

@@ -1,4 +1,4 @@
.update {
.topline {
position: relative;
z-index: 102;
margin-bottom: 0;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Topline.css';
const Topline = props => (
<div className={`alert alert-${props.type} topline`}>
<div className="container">
{props.children}
</div>
</div>
);
Topline.propTypes = {
children: PropTypes.node.isRequired,
type: PropTypes.string.isRequired,
};
export default Topline;

View File

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

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
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>
</Topline>
);
UpdateTopline.propTypes = {
version: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};
export default withNamespaces()(UpdateTopline);

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -3,8 +3,8 @@ import * as actionCreators from '../actions';
import App from '../components/App';
const mapStateToProps = (state) => {
const { dashboard } = state;
const props = { dashboard };
const { dashboard, encryption } = state;
const props = { dashboard, encryption };
return props;
};

View File

@@ -12,11 +12,26 @@ import {
setDhcpConfig,
findActiveDhcp,
} from '../actions';
import {
getTlsStatus,
setTlsConfig,
validateTlsConfig,
} from '../actions/encryption';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const { settings, dashboard, dhcp } = state;
const props = { settings, dashboard, dhcp };
const {
settings,
dashboard,
dhcp,
encryption,
} = state;
const props = {
settings,
dashboard,
dhcp,
encryption,
};
return props;
};
@@ -32,6 +47,9 @@ const mapDispatchToProps = {
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
getTlsStatus,
setTlsConfig,
validateTlsConfig,
};
export default connect(

View File

@@ -60,3 +60,90 @@ export const LANGUAGES = [
name: '正體中文',
},
];
export const INSTALL_FIRST_STEP = 1;
export const INSTALL_TOTAL_STEPS = 5;
export const SETTINGS_NAMES = {
filtering: 'filtering',
safebrowsing: 'safebrowsing',
parental: 'parental',
safesearch: 'safesearch',
};
export const STANDARD_DNS_PORT = 53;
export const STANDARD_WEB_PORT = 80;
export const STANDARD_HTTPS_PORT = 443;
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300;
export const CHECK_TIMEOUT = 1000;
export const STOP_TIMEOUT = 10000;
export const UNSAFE_PORTS = [
1,
7,
9,
11,
13,
15,
17,
19,
20,
21,
22,
23,
25,
37,
42,
43,
53,
77,
79,
87,
95,
101,
102,
103,
104,
109,
110,
111,
113,
115,
117,
119,
123,
135,
139,
143,
179,
389,
465,
512,
513,
514,
515,
526,
530,
531,
532,
540,
556,
563,
587,
601,
636,
993,
995,
2049,
3659,
4045,
6000,
6665,
6666,
6667,
6668,
6669,
];

View File

@@ -0,0 +1,79 @@
import React, { Fragment } from 'react';
import { Trans } from 'react-i18next';
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
export const renderField = ({
input, id, className, placeholder, type, disabled, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const renderSelectField = ({
input, placeholder, disabled, meta: { touched, error },
}) => (
<Fragment>
<label className="checkbox checkbox--form">
<span className="checkbox__marker"/>
<input
{...input}
type="checkbox"
className="checkbox__input"
disabled={disabled}
/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-title">{placeholder}</span>
</span>
</span>
</label>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
export const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
export const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;
}
return false;
};
export const port = (value) => {
if ((value || value === 0) && (value < 80 || value > 65535)) {
return <Trans>form_error_port_range</Trans>;
}
return false;
};
export const isSafePort = (value) => {
if (UNSAFE_PORTS.includes(value)) {
return <Trans>form_error_port_unsafe</Trans>;
}
return false;
};
export const toNumber = value => value && parseInt(value, 10);

View File

@@ -3,8 +3,15 @@ import dateFormat from 'date-fns/format';
import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours';
import round from 'lodash/round';
import axios from 'axios';
import { STATS_NAMES } from './constants';
import {
STATS_NAMES,
STANDARD_DNS_PORT,
STANDARD_WEB_PORT,
STANDARD_HTTPS_PORT,
CHECK_TIMEOUT,
} from './constants';
export const formatTime = (time) => {
const parsedTime = dateParse(time);
@@ -85,3 +92,112 @@ export const getPercent = (amount, number) => {
};
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');
export const getInterfaceIp = (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 interfaceIP;
};
export const getIpList = (interfaces) => {
let list = [];
Object.keys(interfaces).forEach((item) => {
list = [...list, ...interfaces[item].ip_addresses];
});
return list.sort();
};
export const getDnsAddress = (ip, port = '') => {
const isStandardDnsPort = port === STANDARD_DNS_PORT;
let address = ip;
if (port) {
if (ip.includes(':') && !isStandardDnsPort) {
address = `[${ip}]:${port}`;
} else if (!isStandardDnsPort) {
address = `${ip}:${port}`;
}
}
return address;
};
export const getWebAddress = (ip, port = '') => {
const isStandardWebPort = port === STANDARD_WEB_PORT;
let address = `http://${ip}`;
if (port) {
if (ip.includes(':') && !isStandardWebPort) {
address = `http://[${ip}]:${port}`;
} else if (!isStandardWebPort) {
address = `http://${ip}:${port}`;
}
}
return address;
};
export const checkRedirect = (url, attempts) => {
let count = attempts || 1;
if (count > 10) {
window.location.replace(url);
return false;
}
const rmTimeout = t => t && clearTimeout(t);
const setRecursiveTimeout = (time, ...args) => setTimeout(
checkRedirect,
time,
...args,
);
let timeout;
axios.get(url)
.then((response) => {
rmTimeout(timeout);
if (response) {
window.location.replace(url);
return;
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
})
.catch((error) => {
rmTimeout(timeout);
if (error.response) {
window.location.replace(url);
return;
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
});
return false;
};
export const redirectToCurrentProtocol = (values, httpPort = 80) => {
const {
protocol, hostname, hash, port,
} = window.location;
const { enabled, port_https } = values;
const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';
if (protocol !== 'https:' && enabled && port_https) {
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
} else if (protocol === 'https:' && enabled && port_https && port_https !== parseInt(port, 10)) {
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
} else if (protocol === 'https:' && (!enabled || !port_https)) {
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
const AddressList = (props) => {
let webAddress = getWebAddress(props.address, props.port);
let dnsAddress = getDnsAddress(props.address, props.port);
if (props.address === '0.0.0.0') {
return getIpList(props.interfaces).map((ip) => {
webAddress = getWebAddress(ip, props.port);
dnsAddress = getDnsAddress(ip, props.port);
if (props.isDns) {
return (
<li key={ip}>
<strong>
{dnsAddress}
</strong>
</li>
);
}
return (
<li key={ip}>
<a href={webAddress}>
{webAddress}
</a>
</li>
);
});
}
if (props.isDns) {
return (
<strong>
{dnsAddress}
</strong>
);
}
return (
<a href={webAddress}>
{webAddress}
</a>
);
};
AddressList.propTypes = {
interfaces: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
port: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
isDns: PropTypes.bool,
};
export default AddressList;

View File

@@ -0,0 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import i18n from '../../i18n';
import Controls from './Controls';
import renderField from './renderField';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const validate = (values) => {
const errors = {};
if (values.confirm_password !== values.password) {
errors.confirm_password = i18n.t('form_error_password');
}
return errors;
};
const Auth = (props) => {
const {
handleSubmit,
pristine,
invalid,
t,
} = props;
return (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_auth_title</Trans>
</div>
<p className="setup__desc">
<Trans>install_auth_desc</Trans>
</p>
<div className="form-group">
<label>
<Trans>install_auth_username</Trans>
</label>
<Field
name="username"
component={renderField}
type="text"
className="form-control"
placeholder={ t('install_auth_username_enter') }
validate={[required]}
autoComplete="username"
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_password</Trans>
</label>
<Field
name="password"
component={renderField}
type="password"
className="form-control"
placeholder={ t('install_auth_password_enter') }
validate={[required]}
autoComplete="new-password"
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_confirm</Trans>
</label>
<Field
name="confirm_password"
component={renderField}
type="password"
className="form-control"
placeholder={ t('install_auth_confirm') }
validate={[required]}
autoComplete="new-password"
/>
</div>
</div>
<Controls pristine={pristine} invalid={invalid} />
</form>
);
};
Auth.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate,
}),
])(Auth);

View File

@@ -0,0 +1,113 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import * as actionCreators from '../../actions/install';
class Controls extends Component {
renderPrevButton(step) {
switch (step) {
case 2:
case 3:
return (
<button
type="button"
className="btn btn-secondary btn-lg setup__button"
onClick={this.props.prevStep}
>
<Trans>back</Trans>
</button>
);
default:
return false;
}
}
renderNextButton(step) {
switch (step) {
case 1:
return (
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={this.props.nextStep}
>
<Trans>get_started</Trans>
</button>
);
case 2:
case 3:
return (
<button
type="submit"
className="btn btn-success btn-lg setup__button"
disabled={
this.props.invalid
|| this.props.pristine
|| this.props.install.processingSubmit
}
>
<Trans>next</Trans>
</button>
);
case 4:
return (
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={this.props.nextStep}
>
<Trans>next</Trans>
</button>
);
case 5:
return (
<button
type="button"
className="btn btn-success btn-lg setup__button"
onClick={() => this.props.openDashboard(this.props.address)}
>
<Trans>open_dashboard</Trans>
</button>
);
default:
return false;
}
}
render() {
const { install } = this.props;
return (
<div className="setup__nav">
<div className="btn-list">
{this.renderPrevButton(install.step)}
{this.renderNextButton(install.step)}
</div>
</div>
);
}
}
Controls.propTypes = {
install: PropTypes.object.isRequired,
nextStep: PropTypes.func,
prevStep: PropTypes.func,
openDashboard: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
address: PropTypes.string,
};
const mapStateToProps = (state) => {
const { install } = state;
const props = { install };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Controls);

View File

@@ -0,0 +1,134 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import Tabs from '../../components/ui/Tabs';
import Icons from '../../components/ui/Icons';
import Controls from './Controls';
import AddressList from './AddressList';
let Devices = props => (
<div className="setup__step">
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_devices_title</Trans>
</div>
<div className="setup__desc">
<Trans>install_devices_desc</Trans>
<div className="mt-1">
<Trans>install_devices_address</Trans>:
</div>
<div className="mt-1">
<AddressList
interfaces={props.interfaces}
address={props.dnsIp}
port={props.dnsPort}
isDns={true}
/>
</div>
</div>
<Icons />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_devices_router</Trans>
</div>
<div className="tab__text">
<p><Trans>install_devices_router_desc</Trans></p>
<ol>
<li><Trans>install_devices_router_list_1</Trans></li>
<li><Trans>install_devices_router_list_2</Trans></li>
<li><Trans>install_devices_router_list_3</Trans></li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_windows_list_1</Trans></li>
<li><Trans>install_devices_windows_list_2</Trans></li>
<li><Trans>install_devices_windows_list_3</Trans></li>
<li><Trans>install_devices_windows_list_4</Trans></li>
<li><Trans>install_devices_windows_list_5</Trans></li>
<li><Trans>install_devices_windows_list_6</Trans></li>
</ol>
</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_macos_list_1</Trans></li>
<li><Trans>install_devices_macos_list_2</Trans></li>
<li><Trans>install_devices_macos_list_3</Trans></li>
<li><Trans>install_devices_macos_list_4</Trans></li>
</ol>
</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_android_list_1</Trans></li>
<li><Trans>install_devices_android_list_2</Trans></li>
<li><Trans>install_devices_android_list_3</Trans></li>
<li><Trans>install_devices_android_list_4</Trans></li>
<li><Trans>install_devices_android_list_5</Trans></li>
</ol>
</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_ios_list_1</Trans></li>
<li><Trans>install_devices_ios_list_2</Trans></li>
<li><Trans>install_devices_ios_list_3</Trans></li>
<li><Trans>install_devices_ios_list_4</Trans></li>
</ol>
</div>
</div>
</Tabs>
</div>
<Controls />
</div>
);
Devices.propTypes = {
interfaces: PropTypes.object.isRequired,
dnsIp: PropTypes.string.isRequired,
dnsPort: PropTypes.number.isRequired,
};
const selector = formValueSelector('install');
Devices = connect((state) => {
const dnsIp = selector(state, 'dns.ip');
const dnsPort = selector(state, 'dns.port');
return {
dnsIp,
dnsPort,
};
})(Devices);
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Devices);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import Controls from './Controls';
const Greeting = () => (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_welcome_title</Trans>
</h1>
<p className="setup__desc text-center">
<Trans>install_welcome_desc</Trans>
</p>
</div>
<Controls />
</div>
);
export default withNamespaces()(Greeting);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
const getProgressPercent = step => (step / INSTALL_TOTAL_STEPS) * 100;
const Progress = props => (
<div className="setup__progress">
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
<div className="setup__progress-wrap">
<div
className="setup__progress-inner"
style={{ width: `${getProgressPercent(props.step)}%` }}
/>
</div>
</div>
);
Progress.propTypes = {
step: PropTypes.number.isRequired,
};
export default withNamespaces()(Progress);

View File

@@ -0,0 +1,221 @@
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 Controls from './Controls';
import AddressList from './AddressList';
import renderField from './renderField';
import { getInterfaceIp } from '../../helpers/helpers';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const port = (value) => {
if (value < 1 || value > 65535) {
return <Trans>form_error_port</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
if (option.ip_addresses && option.ip_addresses.length > 0) {
const ip = getInterfaceIp(option);
return (
<option value={ip} key={name}>
{name} - {ip}
</option>
);
}
return false;
})
));
let Settings = (props) => {
const {
handleSubmit,
webIp,
webPort,
dnsIp,
dnsPort,
interfaces,
invalid,
webWarning,
dnsWarning,
} = 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>
</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>
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={webIp}
port={webPort}
/>
</div>
{webWarning &&
<div className="text-danger mt-2">
{webWarning}
</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>
</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>
</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>
);
};
Settings.propTypes = {
handleSubmit: PropTypes.func.isRequired,
webIp: PropTypes.string.isRequired,
dnsIp: PropTypes.string.isRequired,
webPort: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
dnsPort: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
webWarning: PropTypes.string.isRequired,
dnsWarning: PropTypes.string.isRequired,
interfaces: PropTypes.object.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object,
};
const selector = formValueSelector('install');
Settings = connect((state) => {
const webIp = selector(state, 'web.ip');
const webPort = selector(state, 'web.port');
const dnsIp = selector(state, 'dns.ip');
const dnsPort = selector(state, 'dns.port');
return {
webIp,
webPort,
dnsIp,
dnsPort,
};
})(Settings);
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Settings);

View File

@@ -0,0 +1,117 @@
.setup {
min-height: calc(100vh - 80px);
line-height: 1.48;
}
@media screen and (min-width: 768px) {
.setup {
padding: 50px 0;
}
}
.setup__container {
max-width: 650px;
margin: 0 auto;
padding: 30px 20px;
line-height: 1.6;
background-color: #fff;
box-shadow: 0 1px 4px rgba(74, 74, 74, 0.36);
border-radius: 3px;
}
@media screen and (min-width: 768px) {
.setup__container {
width: 650px;
padding: 40px 30px;
}
}
.setup__logo {
display: block;
margin: 0 auto 40px;
max-width: 140px;
}
.setup__nav {
text-align: center;
}
.setup__step {
margin-bottom: 25px;
}
.setup__title {
margin-bottom: 30px;
font-size: 28px;
text-align: center;
font-weight: 700;
}
.setup__subtitle {
margin-bottom: 10px;
font-size: 17px;
font-weight: 700;
}
.setup__desc {
margin-bottom: 20px;
font-size: 15px;
}
.setup__group {
margin-bottom: 35px;
}
.setup__group:last-child {
margin-bottom: 0;
}
.setup__progress {
font-size: 13px;
text-align: center;
}
.setup__progress-wrap {
height: 4px;
margin: 20px -20px -30px -20px;
overflow: hidden;
background-color: #eaeaea;
border-radius: 0 0 3px 3px;
}
@media screen and (min-width: 768px) {
.setup__progress-wrap {
margin: 20px -30px -40px -30px;
}
}
.setup__progress-inner {
width: 0;
height: 100%;
font-size: 1.2rem;
line-height: 20px;
color: #fff;
text-align: center;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
transition: width 0.6s ease;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}
.btn-standard {
padding-left: 20px;
padding-right: 20px;
}
.form__message {
font-size: 11px;
}
.form__message--error {
color: #cd201f;
}
.setup__button {
min-width: 120px;
padding-left: 30px;
padding-right: 30px;
}

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { reduxForm, formValueSelector } from 'redux-form';
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">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_submit_title</Trans>
</h1>
<p className="setup__desc">
<Trans>install_submit_desc</Trans>
</p>
</div>
<Controls
openDashboard={props.openDashboard}
address={getWebAddress(props.webIp, props.webPort)}
/>
</div>
);
Submit.propTypes = {
webIp: PropTypes.string.isRequired,
webPort: PropTypes.number.isRequired,
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
openDashboard: PropTypes.func.isRequired,
};
const selector = formValueSelector('install');
Submit = connect((state) => {
const webIp = selector(state, 'web.ip');
const webPort = selector(state, 'web.port');
return {
webIp,
webPort,
};
})(Submit);
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Submit);

View File

@@ -0,0 +1,125 @@
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as actionCreators from '../../actions/install';
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
import Loading from '../../components/ui/Loading';
import Greeting from './Greeting';
import Settings from './Settings';
import Auth from './Auth';
import Devices from './Devices';
import Submit from './Submit';
import Progress from './Progress';
import Toasts from '../../components/Toasts';
import Footer from '../../components/ui/Footer';
import logo from '../../components/ui/svg/logo.svg';
import './Setup.css';
import '../../components/ui/Tabler.css';
class Setup extends Component {
componentDidMount() {
this.props.getDefaultAddresses();
}
handleFormSubmit = (values) => {
this.props.setAllSettings(values);
};
openDashboard = (address) => {
window.location.replace(address);
}
nextStep = () => {
if (this.props.install.step < INSTALL_TOTAL_STEPS) {
this.props.nextStep();
}
}
prevStep = () => {
if (this.props.install.step > INSTALL_FIRST_STEP) {
this.props.prevStep();
}
}
renderPage(step, config, interfaces) {
switch (step) {
case 1:
return <Greeting />;
case 2:
return (
<Settings
initialValues={config}
interfaces={interfaces}
webWarning={config.web.warning}
dnsWarning={config.dns.warning}
onSubmit={this.nextStep}
/>
);
case 3:
return (
<Auth onSubmit={this.handleFormSubmit} />
);
case 4:
return <Devices interfaces={interfaces} />;
case 5:
return <Submit openDashboard={this.openDashboard} />;
default:
return false;
}
}
render() {
const {
processingDefault,
step,
web,
dns,
interfaces,
} = this.props.install;
return (
<Fragment>
{processingDefault && <Loading />}
{!processingDefault &&
<Fragment>
<div className="setup">
<div className="setup__container">
<img src={logo} className="setup__logo" alt="logo" />
{this.renderPage(step, { web, dns }, interfaces)}
<Progress step={step} />
</div>
</div>
<Footer />
<Toasts />
</Fragment>
}
</Fragment>
);
}
}
Setup.propTypes = {
getDefaultAddresses: PropTypes.func.isRequired,
setAllSettings: PropTypes.func.isRequired,
nextStep: PropTypes.func.isRequired,
prevStep: PropTypes.func.isRequired,
install: PropTypes.object.isRequired,
step: PropTypes.number,
web: PropTypes.object,
dns: PropTypes.object,
};
const mapStateToProps = (state) => {
const { install, toasts } = state;
const props = { install, toasts };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Setup);

View File

@@ -0,0 +1,19 @@
import React, { Fragment } from 'react';
const renderField = ({
input, className, placeholder, type, disabled, autoComplete, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export default renderField;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import '../components/App/index.css';
import '../components/ui/ReactTable.css';
import configureStore from '../configureStore';
import reducers from '../reducers/install';
import '../i18n';
import Setup from './Setup';
const store = configureStore(reducers, {}); // set initial state
ReactDOM.render(
<Provider store={store}>
<Setup />
</Provider>,
document.getElementById('root'),
);

View File

@@ -0,0 +1,81 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/encryption';
const encryption = handleActions({
[actions.getTlsStatusRequest]: state => ({ ...state, processing: true }),
[actions.getTlsStatusFailure]: state => ({ ...state, processing: false }),
[actions.getTlsStatusSuccess]: (state, { payload }) => {
const newState = {
...state,
...payload,
processing: false,
};
return newState;
},
[actions.setTlsConfigRequest]: state => ({ ...state, processingConfig: true }),
[actions.setTlsConfigFailure]: state => ({ ...state, processingConfig: false }),
[actions.setTlsConfigSuccess]: (state, { payload }) => {
const newState = {
...state,
...payload,
processingConfig: false,
};
return newState;
},
[actions.validateTlsConfigRequest]: state => ({ ...state, processingValidate: true }),
[actions.validateTlsConfigFailure]: state => ({ ...state, processingValidate: false }),
[actions.validateTlsConfigSuccess]: (state, { payload }) => {
const {
issuer = '',
key_type = '',
not_after = '',
not_before = '',
subject = '',
warning_validation = '',
dns_names = '',
...values
} = payload;
const newState = {
...state,
...values,
issuer,
key_type,
not_after,
not_before,
subject,
warning_validation,
dns_names,
processingValidate: false,
};
return newState;
},
}, {
processing: true,
processingConfig: false,
processingValidate: false,
enabled: false,
dns_names: null,
force_https: false,
issuer: '',
key_type: '',
not_after: '',
not_before: '',
port_dns_over_tls: '',
port_https: '',
subject: '',
valid_chain: false,
valid_key: false,
valid_cert: false,
status_cert: '',
status_key: '',
certificate_chain: '',
private_key: '',
server_name: '',
warning_validation: '',
});
export default encryption;

View File

@@ -1,11 +1,12 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { loadingBarReducer } from 'react-redux-loading-bar';
import nanoid from 'nanoid';
import { reducer as formReducer } from 'redux-form';
import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
import toasts from './toasts';
import encryption from './encryption';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@@ -52,6 +53,7 @@ const dashboard = handleActions({
upstream_dns: upstreamDns,
protection_enabled: protectionEnabled,
language,
http_port: httpPort,
} = payload;
const newState = {
...state,
@@ -64,6 +66,7 @@ const dashboard = handleActions({
upstreamDns: upstreamDns.join('\n'),
protectionEnabled,
language,
httpPort,
};
return newState;
},
@@ -117,13 +120,13 @@ const dashboard = handleActions({
if (versionCompare(currentVersion, payload.version) === -1) {
const {
announcement,
version,
announcement_url: announcementUrl,
} = payload;
const newState = {
...state,
announcement,
version,
announcementUrl,
isUpdateAvailable: true,
};
@@ -140,8 +143,14 @@ const dashboard = handleActions({
return newState;
},
[actions.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }),
[actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }),
[actions.toggleProtectionSuccess]: (state) => {
const newState = { ...state, protectionEnabled: !state.protectionEnabled };
const newState = {
...state,
protectionEnabled: !state.protectionEnabled,
processingProtection: false,
};
return newState;
},
@@ -164,6 +173,8 @@ const dashboard = handleActions({
processingFiltering: true,
upstreamDns: [],
protectionEnabled: false,
processingProtection: false,
httpPort: 80,
});
const queryLogs = handleActions({
@@ -228,38 +239,12 @@ const filtering = handleActions({
isFilteringModalOpen: false,
processingFilters: false,
processingRules: false,
processingAddFilter: false,
processingRefreshFilters: false,
filters: [],
userRules: '',
});
const toasts = handleActions({
[actions.addErrorToast]: (state, { payload }) => {
const errorToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'error',
};
const newState = { ...state, notices: [...state.notices, errorToast] };
return newState;
},
[actions.addSuccessToast]: (state, { payload }) => {
const successToast = {
id: nanoid(),
message: payload,
type: 'success',
};
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[actions.removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };
return newState;
},
}, { notices: [] });
const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
@@ -291,16 +276,29 @@ const dhcp = handleActions({
processingStatus: false,
}),
[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 };
const newState = { ...state, config: newConfig, processingDhcp: false };
return newState;
},
[actions.setDhcpConfigRequest]: state => ({ ...state, processingConfig: true }),
[actions.setDhcpConfigFailure]: state => ({ ...state, processingConfig: false }),
[actions.setDhcpConfigSuccess]: (state, { payload }) => {
const { config } = state;
const newConfig = { ...config, ...payload };
const newState = { ...state, config: newConfig, processingConfig: false };
return newState;
},
}, {
processing: true,
processingStatus: false,
processingInterfaces: false,
processingDhcp: false,
processingConfig: false,
config: {
enabled: false,
},
@@ -315,6 +313,7 @@ export default combineReducers({
filtering,
toasts,
dhcp,
encryption,
loadingBar: loadingBarReducer,
form: formReducer,
});

View File

@@ -0,0 +1,47 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { reducer as formReducer } from 'redux-form';
import * as actions from '../actions/install';
import toasts from './toasts';
import { INSTALL_FIRST_STEP } from '../helpers/constants';
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 };
return newState;
},
[actions.nextStep]: state => ({ ...state, step: state.step + 1 }),
[actions.prevStep]: state => ({ ...state, step: state.step - 1 }),
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
}, {
step: INSTALL_FIRST_STEP,
processingDefault: true,
processingSubmit: false,
web: {
ip: '0.0.0.0',
port: 80,
warning: '',
},
dns: {
ip: '0.0.0.0',
port: 53,
warning: '',
},
interfaces: {},
});
export default combineReducers({
install,
toasts,
form: formReducer,
});

View File

@@ -0,0 +1,34 @@
import { handleActions } from 'redux-actions';
import nanoid from 'nanoid';
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
const toasts = handleActions({
[addErrorToast]: (state, { payload }) => {
const errorToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'error',
};
const newState = { ...state, notices: [...state.notices, errorToast] };
return newState;
},
[addSuccessToast]: (state, { payload }) => {
const successToast = {
id: nanoid(),
message: payload,
type: 'success',
};
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };
return newState;
},
}, { notices: [] });
export default toasts;

View File

@@ -8,7 +8,9 @@ const CleanWebpackPlugin = require('clean-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
@@ -16,7 +18,8 @@ const config = {
target: 'web',
context: RESOURCES_PATH,
entry: {
bundle: ENTRY_REACT,
main: ENTRY_REACT,
install: ENTRY_INSTALL,
},
output: {
path: PUBLIC_PATH,
@@ -101,8 +104,16 @@ const config = {
new HtmlWebpackPlugin({
inject: true,
cache: false,
chunks: ['main'],
template: HTML_PATH,
}),
new HtmlWebpackPlugin({
inject: true,
cache: false,
chunks: ['install'],
filename: 'install.html',
template: HTML_INSTALL_PATH,
}),
new ExtractTextPlugin({
filename: '[name].[contenthash].css',
}),

View File

@@ -2,7 +2,6 @@ const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
module: {
rules: [{
test: /\.js$/,

137
config.go
View File

@@ -5,12 +5,13 @@ import (
"os"
"path/filepath"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/hmage/golibs/log"
"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)
const (
@@ -18,22 +19,32 @@ const (
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
)
// logSettings
type logSettings struct {
LogFile string `yaml:"log_file"` // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
}
// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
type configuration struct {
ourConfigFilename string // Config filename (can be overriden via the command line arguments)
ourBinaryDir string // Location of our directory, used to protect against CWD being somewhere else
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
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
AuthName string `yaml:"auth_name"`
AuthPass string `yaml:"auth_pass"`
Language string `yaml:"language"` // two-letter ISO 639-1 language code
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"`
logSettings `yaml:",inline"`
sync.RWMutex `yaml:"-"`
SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
@@ -41,7 +52,8 @@ type configuration struct {
// field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct {
Port int `yaml:"port"`
BindHost string `yaml:"bind_host"`
Port int `yaml:"port"`
dnsforward.FilteringConfig `yaml:",inline"`
@@ -50,13 +62,51 @@ type dnsConfig struct {
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
ForceHTTPS bool `yaml:"force_https" json:"force_https,omitempty"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
PortHTTPS int `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
}
// field ordering is not important -- these are for API and are recalculated on each run
type tlsConfigStatus struct {
ValidCert bool `yaml:"-" json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
ValidChain bool `yaml:"-" json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
Subject string `yaml:"-" json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
Issuer string `yaml:"-" json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
NotBefore time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
NotAfter time.Time `yaml:"-" json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
DNSNames []string `yaml:"-" json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
// key status
ValidKey bool `yaml:"-" json:"valid_key"` // ValidKey is true if the key is a valid private key
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator
usable bool
// warnings
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
}
// field ordering is important -- yaml fields will mirror ordering from here
type tlsConfig struct {
tlsConfigSettings `yaml:",inline" json:",inline"`
tlsConfigStatus `yaml:"-" json:",inline"`
}
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
ourConfigFilename: "AdGuardHome.yaml",
BindPort: 3000,
BindHost: "127.0.0.1",
BindHost: "0.0.0.0",
DNS: dnsConfig{
Port: 53,
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
@@ -68,6 +118,12 @@ var config = configuration{
},
UpstreamDNS: defaultDNS,
},
TLS: tlsConfig{
tlsConfigSettings: tlsConfigSettings{
PortHTTPS: 443,
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
},
},
Filters: []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
@@ -77,20 +133,43 @@ var config = configuration{
SchemaVersion: currentSchemaVersion,
}
// Loads configuration from the YAML file
func parseConfig() error {
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Reading YAML file: %s", configFile)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
// do nothing, file doesn't exist
log.Printf("YAML file doesn't exist, skipping: %s", configFile)
return nil
// getConfigFilename returns path to the current config file
func (c *configuration) getConfigFilename() string {
configFile := config.ourConfigFilename
if !filepath.IsAbs(configFile) {
configFile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
}
yamlFile, err := ioutil.ReadFile(configFile)
return configFile
}
// getLogSettings reads logging settings from the config file.
// we do it in a separate method in order to configure logger before the actual configuration is parsed and applied.
func getLogSettings() logSettings {
l := logSettings{}
yamlFile, err := readConfigFile()
if err != nil || yamlFile == nil {
return l
}
err = yaml.Unmarshal(yamlFile, &l)
if err != nil {
log.Printf("Couldn't get logging settings from the configuration: %s", err)
}
return l
}
// parseConfig loads configuration from the YAML file
func parseConfig() error {
configFile := config.getConfigFilename()
log.Printf("Reading config file: %s", configFile)
yamlFile, err := readConfigFile()
if err != nil {
log.Printf("Couldn't read config file: %s", err)
return err
}
if yamlFile == nil {
log.Printf("YAML file doesn't exist, skipping it")
return nil
}
err = yaml.Unmarshal(yamlFile, &config)
if err != nil {
log.Printf("Couldn't parse config file: %s", err)
@@ -105,12 +184,26 @@ func parseConfig() error {
return nil
}
// 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
}
return ioutil.ReadFile(configFile)
}
// 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()
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Writing YAML file: %s", configFile)
if config.firstRun {
log.Tracef("Silently refusing to write config because first run and not configured yet")
return nil
}
configFile := config.getConfigFilename()
log.Tracef("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
if err != nil {
log.Printf("Couldn't generate YAML file: %s", err)

File diff suppressed because it is too large Load Diff

89
dhcp.go
View File

@@ -58,7 +58,10 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
}
}
if !newconfig.Enabled {
dhcpServer.Stop()
err := dhcpServer.Stop()
if err != nil {
log.Printf("failed to stop the DHCP server: %s", err)
}
}
config.DHCP = newconfig
httpUpdateConfigReloadDNSReturnOK(w, r)
@@ -67,55 +70,54 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{}
ifaces, err := net.Interfaces()
ifaces, err := getValidNetInterfaces()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get list of interfaces: %s", err)
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
}
type address struct {
IP string
Netmask string
}
type responseInterface struct {
Name string `json:"name"`
MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"`
Addresses []string `json:"ip_addresses"`
}
for i := range ifaces {
if ifaces[i].Flags&net.FlagLoopback != 0 {
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
// it's a loopback, skip it
continue
}
if ifaces[i].Flags&net.FlagBroadcast == 0 {
if iface.Flags&net.FlagBroadcast == 0 {
// this interface doesn't support broadcast, skip it
continue
}
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
// this interface is ppp, don't do dhcp over it
continue
}
iface := responseInterface{
Name: ifaces[i].Name,
MTU: ifaces[i].MTU,
HardwareAddr: ifaces[i].HardwareAddr.String(),
}
addrs, err := ifaces[i].Addrs()
addrs, err := iface.Addrs()
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %v: %s", ifaces[i].Name, err)
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err)
return
}
jsonIface := netInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
}
if iface.Flags != 0 {
jsonIface.Flags = iface.Flags.String()
}
// we don't want link-local addresses in json, so skip them
for _, addr := range addrs {
iface.Addresses = append(iface.Addresses, addr.String())
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
return
}
// ignore link-local
if ipnet.IP.IsLinkLocalUnicast() {
continue
}
jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String())
}
if len(iface.Addresses) == 0 {
// this interface has no addresses, skip it
continue
if len(jsonIface.Addresses) != 0 {
response[iface.Name] = jsonIface
}
response[ifaces[i].Name] = iface
}
err = json.NewEncoder(w).Encode(response)
@@ -157,7 +159,7 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
}
func startDHCPServer() error {
if config.DHCP.Enabled == false {
if !config.DHCP.Enabled {
// not enabled, don't do anything
return nil
}
@@ -167,3 +169,20 @@ func startDHCPServer() error {
}
return nil
}
func stopDHCPServer() error {
if !config.DHCP.Enabled {
return nil
}
if !dhcpServer.Enabled {
return nil
}
err := dhcpServer.Stop()
if err != nil {
return errorx.Decorate(err, "Couldn't stop DHCP server")
}
return nil
}

View File

@@ -13,6 +13,8 @@ import (
"github.com/krolaw/dhcp4"
)
// CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface,
// and waits for a response for a period defined by defaultDiscoverTime
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
@@ -30,8 +32,8 @@ 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)
n, err := rand.Read(xId)
xID := make([]byte, 8)
n, err := rand.Read(xID)
if n != 8 && err == nil {
err = fmt.Errorf("Generated less than 8 bytes")
}
@@ -57,16 +59,16 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
maxUDPsizeRaw := make([]byte, 2)
binary.BigEndian.PutUint16(maxUDPsizeRaw, 1500)
leaseTimeRaw := make([]byte, 4)
leaseTime := uint32(math.RoundToEven(time.Duration(time.Hour * 24 * 90).Seconds()))
leaseTime := uint32(math.RoundToEven((time.Hour * 24 * 90).Seconds()))
binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime)
options := []dhcp4.Option{
{dhcp4.OptionParameterRequestList, requestList},
{dhcp4.OptionMaximumDHCPMessageSize, maxUDPsizeRaw},
{dhcp4.OptionClientIdentifier, append([]byte{0x01}, iface.HardwareAddr...)},
{dhcp4.OptionIPAddressLeaseTime, leaseTimeRaw},
{dhcp4.OptionHostName, []byte(hostname)},
{Code: dhcp4.OptionParameterRequestList, Value: requestList},
{Code: dhcp4.OptionMaximumDHCPMessageSize, Value: maxUDPsizeRaw},
{Code: dhcp4.OptionClientIdentifier, Value: append([]byte{0x01}, iface.HardwareAddr...)},
{Code: dhcp4.OptionIPAddressLeaseTime, Value: leaseTimeRaw},
{Code: dhcp4.OptionHostName, Value: []byte(hostname)},
}
packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xId, false, options)
packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xID, false, options)
// resolve 0.0.0.0:68
udpAddr, err := net.ResolveUDPAddr("udp4", src)
@@ -98,7 +100,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
}
// send to 255.255.255.255:67
n, err = c.WriteTo(packet, dstAddr)
_, err = c.WriteTo(packet, dstAddr)
// spew.Dump(n, err)
if err != nil {
return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst)

View File

@@ -13,6 +13,7 @@ import (
const defaultDiscoverTime = time.Second * 3
// Lease contains the necessary information about a DHCP lease
// field ordering is important -- yaml fields will mirror ordering from here
type Lease struct {
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
@@ -21,6 +22,7 @@ type Lease struct {
Expiry time.Time `json:"expires"`
}
// ServerConfig - DHCP server configuration
// field ordering is important -- yaml fields will mirror ordering from here
type ServerConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
@@ -32,6 +34,7 @@ type ServerConfig struct {
LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds
}
// Server - the current state of the DHCP server
type Server struct {
conn *filterConn // listening UDP socket
@@ -80,6 +83,7 @@ func (s *Server) Start(config *ServerConfig) error {
s.leaseStart, err = parseIPv4(s.RangeStart)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
}
@@ -137,6 +141,7 @@ func (s *Server) Start(config *ServerConfig) error {
return nil
}
// Stop closes the listening UDP socket
func (s *Server) Stop() error {
if s.conn == nil {
// nothing to do, return silently
@@ -174,7 +179,7 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
}
// not assigned a lease, create new one, find IP from LRU
log.Tracef("Lease not found for %s: creating new one", hwaddr)
ip, err := s.findFreeIP(p, hwaddr)
ip, err := s.findFreeIP(hwaddr)
if err != nil {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
}
@@ -198,7 +203,7 @@ func (s *Server) locateLease(p dhcp4.Packet) *Lease {
return nil
}
func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, error) {
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)
@@ -223,7 +228,7 @@ func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, er
if foundIP == nil {
// TODO: LRU
return nil, fmt.Errorf("Couldn't find free entry in IP pool")
return nil, fmt.Errorf("couldn't find free entry in IP pool")
}
s.reserveIP(foundIP, hwaddr)
@@ -249,6 +254,7 @@ func (s *Server) unreserveIP(ip net.IP) {
delete(s.IPpool, IP4)
}
// ServeDHCP handles an incoming DHCP request
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got %v message", msgType)
log.Tracef("Leases:")
@@ -259,27 +265,6 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
for ip, hwaddr := range s.IPpool {
log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr)
}
// spew.Dump(s.leases, s.IPpool)
// log.Printf("Called with msgType = %v, options = %+v", msgType, options)
// spew.Dump(p)
// log.Printf("%14s %v", "p.Broadcast", p.Broadcast()) // false
// log.Printf("%14s %v", "p.CHAddr", p.CHAddr()) // 2c:f0:a2:f2:31:00
// log.Printf("%14s %v", "p.CIAddr", p.CIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.Cookie", p.Cookie()) // [99 130 83 99]
// log.Printf("%14s %v", "p.File", p.File()) // []
// log.Printf("%14s %v", "p.Flags", p.Flags()) // [0 0]
// log.Printf("%14s %v", "p.GIAddr", p.GIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.HLen", p.HLen()) // 6
// log.Printf("%14s %v", "p.HType", p.HType()) // 1
// log.Printf("%14s %v", "p.Hops", p.Hops()) // 0
// log.Printf("%14s %v", "p.OpCode", p.OpCode()) // BootRequest
// log.Printf("%14s %v", "p.Options", p.Options()) // [53 1 1 55 10 1 121 3 6 15 119 252 95 44 46 57 2 5 220 61 7 1 44 240 162 242 49 0 51 4 0 118 167 0 12 4 119 104 109 100 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// log.Printf("%14s %v", "p.ParseOptions", p.ParseOptions()) // map[OptionParameterRequestList:[1 121 3 6 15 119 252 95 44 46] OptionDHCPMessageType:[1] OptionMaximumDHCPMessageSize:[5 220] OptionClientIdentifier:[1 44 240 162 242 49 0] OptionIPAddressLeaseTime:[0 118 167 0] OptionHostName:[119 104 109 100]]
// log.Printf("%14s %v", "p.SIAddr", p.SIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.SName", p.SName()) // []
// log.Printf("%14s %v", "p.Secs", p.Secs()) // [0 8]
// log.Printf("%14s %v", "p.XId", p.XId()) // [211 184 20 44]
// log.Printf("%14s %v", "p.YIAddr", p.YIAddr()) // 0.0.0.0
switch msgType {
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
@@ -297,75 +282,7 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
// start/renew a lease -- update lease time
// some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request
log.Tracef("Got from client: Request")
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
return nil // Message not for this dhcp server
}
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
if reqIP == nil {
reqIP = net.IP(p.CIAddr())
}
if reqIP.To4() == nil {
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
if reqIP.Equal(net.IPv4zero) {
log.Tracef("Replying with NAK: request IP is 0.0.0.0")
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
log.Tracef("requested IP is %s", reqIP)
lease, err := s.reserveLease(p)
if err != nil {
log.Tracef("Couldn't find free lease: %s", err)
// couldn't find lease, don't respond
return nil
}
if lease.IP.Equal(reqIP) {
// IP matches lease IP, nothing else to do
lease.Expiry = time.Now().Add(s.leaseTime)
log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
//
// requested IP different from lease
//
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
hwaddr := s.getIPpool(reqIP)
if hwaddr == nil {
// not in pool, check if it's in DHCP range
if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) {
// okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP
log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr())
s.unreserveIP(lease.IP)
lease.IP = reqIP
s.reserveIP(reqIP, p.CHAddr())
lease.Expiry = time.Now().Add(s.leaseTime)
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
}
if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) {
log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr)
}
// requsted IP is not sufficient, reply with NAK
if hwaddr != nil {
log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
// requested IP is outside of DHCP range
log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
return s.handleDHCP4Request(p, options)
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
log.Tracef("Got from client: Decline")
@@ -390,6 +307,79 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
return nil
}
func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got from client: Request")
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
return nil // Message not for this dhcp server
}
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
if reqIP == nil {
reqIP = p.CIAddr()
}
if reqIP.To4() == nil {
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
if reqIP.Equal(net.IPv4zero) {
log.Tracef("Replying with NAK: request IP is 0.0.0.0")
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
log.Tracef("requested IP is %s", reqIP)
lease, err := s.reserveLease(p)
if err != nil {
log.Tracef("Couldn't find free lease: %s", err)
// couldn't find lease, don't respond
return nil
}
if lease.IP.Equal(reqIP) {
// IP matches lease IP, nothing else to do
lease.Expiry = time.Now().Add(s.leaseTime)
log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
//
// requested IP different from lease
//
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
hwaddr := s.getIPpool(reqIP)
if hwaddr == nil {
// not in pool, check if it's in DHCP range
if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) {
// okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP
log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr())
s.unreserveIP(lease.IP)
lease.IP = reqIP
s.reserveIP(reqIP, p.CHAddr())
lease.Expiry = time.Now().Add(s.leaseTime)
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
}
if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) {
log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr)
}
// requsted IP is not sufficient, reply with NAK
if hwaddr != nil {
log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
// requested IP is outside of DHCP range
log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
// Leases returns the list of current DHCP leases
func (s *Server) Leases() []*Lease {
s.RLock()
result := s.leases

View File

@@ -8,7 +8,7 @@ import (
)
// filterConn listens to 0.0.0.0:67, but accepts packets only from specific interface
// This is neccessary for DHCP daemon to work, since binding to IP address doesn't
// This is necessary for DHCP daemon to work, since binding to IP address doesn't
// us access to see Discover/Request packets from clients.
//
// TODO: on windows, controlmessage does not work, try to find out another way
@@ -49,7 +49,6 @@ func (f *filterConn) ReadFrom(b []byte) (int, net.Addr, error) {
}
// packet doesn't match criteria, drop it
}
return 0, nil, nil
}
func (f *filterConn) WriteTo(b []byte, addr net.Addr) (int, error) {

View File

@@ -3,7 +3,6 @@ package dhcpd
import (
"fmt"
"net"
"strings"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
@@ -32,11 +31,11 @@ func getIfaceIPv4(iface *net.Interface) *net.IPNet {
}
if ipnet.IP.To4() == nil {
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
log.Tracef("Got IP that is not IPv4: %v", ipnet.IP)
continue
}
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
log.Tracef("Got IP that is IPv4: %v", ipnet.IP)
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
@@ -45,22 +44,6 @@ func getIfaceIPv4(iface *net.Interface) *net.IPNet {
return nil
}
func isConnClosed(err error) bool {
if err == nil {
return false
}
nerr, ok := err.(*net.OpError)
if !ok {
return false
}
if strings.Contains(nerr.Err.Error(), "use of closed network connection") {
return true
}
return false
}
func wrapErrPrint(err error, message string, args ...interface{}) error {
var errx error
if err == nil {

View File

@@ -76,9 +76,9 @@ func main() {
panic(err)
}
log.Printf("Now serving DHCP")
signal_channel := make(chan os.Signal)
signal.Notify(signal_channel, syscall.SIGINT, syscall.SIGTERM)
<-signal_channel
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
<-signalChannel
}

37
dns.go
View File

@@ -3,6 +3,7 @@ package main
import (
"fmt"
"net"
"os"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
@@ -11,10 +12,22 @@ import (
"github.com/joomcode/errorx"
)
var dnsServer = dnsforward.Server{}
var dnsServer *dnsforward.Server
// initDNSServer creates an instance of the dnsforward.Server
// Please note that we must do it even if we don't start it
// so that we had access to the query log and the stats
func initDNSServer(baseDir string) {
err := os.MkdirAll(baseDir, 0755)
if err != nil {
log.Fatalf("Cannot create DNS data dir at %s: %s", baseDir, err)
}
dnsServer = dnsforward.NewServer(baseDir)
}
func isRunning() bool {
return dnsServer.IsRunning()
return dnsServer != nil && dnsServer.IsRunning()
}
func generateServerConfig() dnsforward.ServerConfig {
@@ -32,26 +45,38 @@ func generateServerConfig() dnsforward.ServerConfig {
}
newconfig := dnsforward.ServerConfig{
UDPListenAddr: &net.UDPAddr{Port: config.DNS.Port},
UDPListenAddr: &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
TCPListenAddr: &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
FilteringConfig: config.DNS.FilteringConfig,
Filters: filters,
}
if config.TLS.Enabled {
newconfig.TLSConfig = config.TLS.TLSConfig
if config.TLS.PortDNSOverTLS != 0 {
newconfig.TLSListenAddr = &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.TLS.PortDNSOverTLS}
}
}
for _, u := range config.DNS.UpstreamDNS {
upstream, err := upstream.AddressToUpstream(u, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
opts := upstream.Options{
Timeout: dnsforward.DefaultTimeout,
Bootstrap: []string{config.DNS.BootstrapDNS},
}
dnsUpstream, err := upstream.AddressToUpstream(u, opts)
if err != nil {
log.Printf("Couldn't get upstream: %s", err)
// continue, just ignore the upstream
continue
}
newconfig.Upstreams = append(newconfig.Upstreams, upstream)
newconfig.Upstreams = append(newconfig.Upstreams, dnsUpstream)
}
return newconfig
}
func startDNSServer() error {
if isRunning() {
return fmt.Errorf("Unable to start forwarding DNS server: Already running")
return fmt.Errorf("unable to start forwarding DNS server: Already running")
}
newconfig := generateServerConfig()

View File

@@ -35,7 +35,7 @@ const defaultParentalURL = "http://%s/check-parental-control-hash?prefixes=%s&se
// ErrInvalidSyntax is returned by AddRule when the rule is invalid
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
// ErrInvalidSyntax is returned by AddRule when the rule was already added to the filter
// ErrAlreadyExists is returned by AddRule when the rule was already added to the filter
var ErrAlreadyExists = errors.New("dnsfilter: rule was already added")
const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet spot
@@ -91,10 +91,11 @@ type LookupStats struct {
PendingMax int64 // maximum number of pending HTTP requests
}
// Stats store LookupStats for both safebrowsing and parental
// Stats store LookupStats for safebrowsing, parental and safesearch
type Stats struct {
Safebrowsing LookupStats
Parental LookupStats
Safesearch LookupStats
}
// Dnsfilter holds added rules and performs hostname matches against the rules
@@ -115,6 +116,7 @@ type Dnsfilter struct {
privateConfig
}
// Filter represents a filter list
type Filter struct {
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
Rules []string `json:"-" yaml:"-"` // not in yaml or json
@@ -127,16 +129,26 @@ type Reason int
const (
// reasons for not filtering
NotFilteredNotFound Reason = iota // host was not find in any checks, default value for result
NotFilteredWhiteList // the host is explicitly whitelisted
NotFilteredError // there was a transitive error during check
// NotFilteredNotFound - host was not find in any checks, default value for result
NotFilteredNotFound Reason = iota
// NotFilteredWhiteList - the host is explicitly whitelisted
NotFilteredWhiteList
// NotFilteredError - there was a transitive error during check
NotFilteredError
// reasons for filtering
FilteredBlackList // the host was matched to be advertising host
FilteredSafeBrowsing // the host was matched to be malicious/phishing
FilteredParental // the host was matched to be outside of parental control settings
FilteredInvalid // the request was invalid and was not processed
FilteredSafeSearch // the host was replaced with safesearch variant
// FilteredBlackList - the host was matched to be advertising host
FilteredBlackList
// FilteredSafeBrowsing - the host was matched to be malicious/phishing
FilteredSafeBrowsing
// FilteredParental - the host was matched to be outside of parental control settings
FilteredParental
// FilteredInvalid - the request was invalid and was not processed
FilteredInvalid
// FilteredSafeSearch - the host was replaced with safesearch variant
FilteredSafeSearch
)
// these variables need to survive coredns reload
@@ -144,6 +156,7 @@ var (
stats Stats
safebrowsingCache gcache.Cache
parentalCache gcache.Cache
safeSearchCache gcache.Cache
)
// Result holds state of hostname check
@@ -151,7 +164,7 @@ type Result struct {
IsFiltered bool `json:",omitempty"` // True if the host name is filtered
Reason Reason `json:",omitempty"` // Reason for blocking / unblocking
Rule string `json:",omitempty"` // Original rule text
Ip net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
IP net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to
}
@@ -177,6 +190,19 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
return result, nil
}
// check safeSearch if no match
if d.SafeSearchEnabled {
result, err = d.checkSafeSearch(host)
if err != nil {
log.Printf("Failed to safesearch HTTP lookup, ignoring check: %v", err)
return Result{}, nil
}
if result.Reason.Matched() {
return result, nil
}
}
// check safebrowsing if no match
if d.SafeBrowsingEnabled {
result, err = d.checkSafeBrowsing(host)
@@ -228,7 +254,6 @@ func newRulesTable() *rulesTable {
func (r *rulesTable) Add(rule *rule) {
r.Lock()
if rule.ip != nil {
// Hosts syntax
r.rulesByHost[rule.text] = rule
@@ -476,7 +501,7 @@ func (rule *rule) match(host string) (Result, error) {
IsFiltered: true,
Reason: FilteredBlackList,
Rule: rule.originalText,
Ip: rule.ip,
IP: rule.ip,
FilterID: rule.listID,
}, nil
}
@@ -574,6 +599,62 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
return hashparam.String(), hashes
}
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
if safeSearchCache == nil {
safeSearchCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
// Check cache. Return cached result if it was found
cachedValue, isFound, err := getCachedReason(safeSearchCache, host)
if isFound {
atomic.AddUint64(&stats.Safesearch.CacheHits, 1)
return cachedValue, nil
}
if err != nil {
return Result{}, err
}
safeHost, ok := d.SafeSearchDomain(host)
if !ok {
return Result{}, nil
}
res := Result{IsFiltered: true, Reason: FilteredSafeSearch}
if ip := net.ParseIP(safeHost); ip != nil {
res.IP = ip
err = safeSearchCache.Set(host, res)
if err != nil {
return Result{}, nil
}
return res, nil
}
// TODO this address should be resolved with upstream that was configured in dnsforward
addrs, err := net.LookupIP(safeHost)
if err != nil {
log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err)
return Result{}, err
}
res.IP = addrs[0]
// The next bug may occurs: LookupIP returns DNS64 mapped ipv4 address with zero-prefix
for _, i := range addrs {
if ipv4 := i.To4(); ipv4 != nil && len(i) == net.IPv6len {
res.IP = ipv4
break
}
}
// Cache result
err = safeSearchCache.Set(host, res)
if err != nil {
return Result{}, nil
}
return res, nil
}
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
// prevent recursion -- checking the host of safebrowsing server makes no sense
if host == d.safeBrowsingServer {
@@ -661,8 +742,11 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
return result, err
}
type formatHandler func(hashparam string) string
type bodyHandler func(body []byte, hashes map[string]bool) (Result, error)
// real implementation of lookup/check
func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gcache.Cache, hashparamNeedSlash bool, format func(hashparam string) string, handleBody func(body []byte, hashes map[string]bool) (Result, error)) (Result, error) {
func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gcache.Cache, hashparamNeedSlash bool, format formatHandler, handleBody bodyHandler) (Result, error) {
// if host ends with a dot, trim it
host = strings.ToLower(strings.Trim(host, "."))
@@ -774,43 +858,43 @@ func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
}
// Start parsing the rule
rule := rule{
r := rule{
text: input, // will be modified
originalText: input,
listID: filterListID,
}
// Mark rule as whitelist if it starts with @@
if strings.HasPrefix(rule.text, "@@") {
rule.isWhitelist = true
rule.text = rule.text[2:]
if strings.HasPrefix(r.text, "@@") {
r.isWhitelist = true
r.text = r.text[2:]
}
err := rule.parseOptions()
err := r.parseOptions()
if err != nil {
return err
}
rule.extractShortcut()
r.extractShortcut()
if !enableDelayedCompilation {
err := rule.compile()
err := r.compile()
if err != nil {
return err
}
}
destination := d.blackList
if rule.isImportant {
if r.isImportant {
destination = d.important
} else if rule.isWhitelist {
} else if r.isWhitelist {
destination = d.whiteList
}
d.storageMutex.Lock()
d.storage[input] = true
d.storageMutex.Unlock()
destination.Add(&rule)
destination.Add(&r)
return nil
}
@@ -835,13 +919,13 @@ func (d *Dnsfilter) parseEtcHosts(input string, filterListID int64) bool {
d.storageMutex.Unlock()
for _, host := range fields[1:] {
rule := rule{
r := rule{
text: host,
originalText: input,
listID: filterListID,
ip: addr,
}
d.blackList.Add(&rule)
d.blackList.Add(&r)
}
return true
}
@@ -879,15 +963,16 @@ func New(c *Config) *Dnsfilter {
d.whiteList = newRulesTable()
d.blackList = newRulesTable()
// Customize the Transport to have larger connection pool
defaultRoundTripper := http.DefaultTransport
defaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)
if !ok {
panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))
// Customize the Transport to have larger connection pool,
// We are not (re)using http.DefaultTransport because of race conditions found by tests
d.transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: defaultHTTPMaxIdleConnections, // default 100
MaxIdleConnsPerHost: defaultHTTPMaxIdleConnections, // default 2
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
d.transport = defaultTransportPointer // dereference it to get a copy of the struct that the pointer points to
d.transport.MaxIdleConns = defaultHTTPMaxIdleConnections // default 100
d.transport.MaxIdleConnsPerHost = defaultHTTPMaxIdleConnections // default 2
d.client = http.Client{
Transport: d.transport,
Timeout: defaultHTTPTimeout,
@@ -913,15 +998,6 @@ func (d *Dnsfilter) Destroy() {
// config manipulation helpers
//
// IsParentalSensitivityValid checks if sensitivity is valid value
func IsParentalSensitivityValid(sensitivity int) bool {
switch sensitivity {
case 3, 10, 13, 17:
return true
}
return false
}
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
if len(host) == 0 {

View File

@@ -4,6 +4,7 @@ import (
"archive/zip"
"bytes"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"path"
@@ -26,7 +27,7 @@ import (
func TestLotsOfRulesMemoryUsage(t *testing.T) {
start := getRSS()
log.Tracef("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
dumpMemProfile("tests/" + _Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
@@ -37,7 +38,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) {
afterLoad := getRSS()
log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
dumpMemProfile("tests/" + _Func() + "2.pprof")
tests := []struct {
host string
@@ -60,7 +61,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) {
}
afterMatch := getRSS()
log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
dumpMemProfile("tests/" + _Func() + "3.pprof")
}
func getRSS() uint64 {
@@ -69,6 +70,9 @@ func getRSS() uint64 {
panic(err)
}
minfo, err := proc.MemoryInfo()
if err != nil {
panic(err)
}
return minfo.RSS
}
@@ -86,7 +90,7 @@ func dumpMemProfile(name string) {
}
}
const topHostsFilename = "../tests/top-1m.csv"
const topHostsFilename = "tests/top-1m.csv"
func fetchTopHostsFromNet() {
log.Tracef("Fetching top hosts from network")
@@ -146,7 +150,7 @@ func getTopHosts() {
func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
start := getRSS()
log.Tracef("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
dumpMemProfile("tests/" + _Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
@@ -155,7 +159,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
afterLoad := getRSS()
log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
dumpMemProfile("tests/" + _Func() + "2.pprof")
getTopHosts()
hostnames, err := os.Open(topHostsFilename)
@@ -165,7 +169,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
defer hostnames.Close()
afterHosts := getRSS()
log.Tracef("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
dumpMemProfile(_Func() + "2.pprof")
dumpMemProfile("tests/" + _Func() + "2.pprof")
{
scanner := bufio.NewScanner(hostnames)
@@ -184,7 +188,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
afterMatch := getRSS()
log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
dumpMemProfile("tests/" + _Func() + "3.pprof")
}
func TestRuleToRegexp(t *testing.T) {
@@ -282,7 +286,7 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
}
}
func (d *Dnsfilter) checkMatchIp(t *testing.T, hostname string, ip string) {
func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string) {
t.Helper()
ret, err := d.CheckHost(hostname)
if err != nil {
@@ -291,8 +295,8 @@ func (d *Dnsfilter) checkMatchIp(t *testing.T, hostname string, ip string) {
if !ret.IsFiltered {
t.Errorf("Expected hostname %s to match", hostname)
}
if ret.Ip == nil || ret.Ip.String() != ip {
t.Errorf("Expected ip %s to match, actual: %v", ip, ret.Ip)
if ret.IP == nil || ret.IP.String() != ip {
t.Errorf("Expected ip %s to match, actual: %v", ip, ret.IP)
}
}
@@ -308,7 +312,7 @@ func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
}
func loadTestRules(d *Dnsfilter) error {
filterFileName := "../tests/dns.txt"
filterFileName := "tests/dns.txt"
file, err := os.Open(filterFileName)
if err != nil {
return err
@@ -368,8 +372,8 @@ func TestEtcHostsMatching(t *testing.T) {
text := fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr)
d.checkAddRule(t, text)
d.checkMatchIp(t, "google.com", addr)
d.checkMatchIp(t, "www.google.com", addr)
d.checkMatchIP(t, "google.com", addr)
d.checkMatchIP(t, "www.google.com", addr)
d.checkMatchEmpty(t, "subdomain.google.com")
d.checkMatchEmpty(t, "example.org")
}
@@ -604,6 +608,155 @@ func TestSafeBrowsingCustomServerFail(t *testing.T) {
d.checkMatchEmpty(t, "wmconvirus.narod.ru")
}
func TestCheckHostSafeSearchYandex(t *testing.T) {
d := NewForTest()
defer d.Destroy()
// Enable safesearch
d.SafeSearchEnabled = true
// Slice of yandex domains
yandex := []string{"yAndeX.ru", "YANdex.COM", "yandex.ua", "yandex.by", "yandex.kz", "www.yandex.com"}
// Check host for each domain
for _, host := range yandex {
result, err := d.CheckHost(host)
if err != nil {
t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err)
}
if result.IP.String() != "213.180.193.56" {
t.Errorf("SafeSearch doesn't work for yandex domain `%s`", host)
}
}
}
func TestCheckHostSafeSearchGoogle(t *testing.T) {
d := NewForTest()
defer d.Destroy()
// Enable safesearch
d.SafeSearchEnabled = true
// Slice of google domains
googleDomains := []string{"www.google.com", "www.google.im", "www.google.co.in", "www.google.iq", "www.google.is", "www.google.it", "www.google.je"}
// Check host for each domain
for _, host := range googleDomains {
result, err := d.CheckHost(host)
if err != nil {
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
}
if result.IP == nil {
t.Errorf("SafeSearch doesn't work for %s", host)
}
}
}
func TestSafeSearchCacheYandex(t *testing.T) {
d := NewForTest()
defer d.Destroy()
domain := "yandex.ru"
var result Result
var err error
// Check host with disabled safesearch
result, err = d.CheckHost(domain)
if err != nil {
t.Fatalf("Cannot check host due to %s", err)
}
if result.IP != nil {
t.Fatalf("SafeSearch is not enabled but there is an answer for `%s` !", domain)
}
// Enable safesearch
d.SafeSearchEnabled = true
result, err = d.CheckHost(domain)
if err != nil {
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
}
// Fir yandex we already know valid ip
if result.IP.String() != "213.180.193.56" {
t.Fatalf("Wrong IP for %s safesearch: %s", domain, result.IP.String())
}
// Check cache
cachedValue, isFound, err := getCachedReason(safeSearchCache, domain)
if err != nil {
t.Fatalf("An error occured during cache search for %s: %s", domain, err)
}
if !isFound {
t.Fatalf("Safesearch cache doesn't work for %s!", domain)
}
if cachedValue.IP.String() != "213.180.193.56" {
t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String())
}
}
func TestSafeSearchCacheGoogle(t *testing.T) {
d := NewForTest()
defer d.Destroy()
domain := "www.google.ru"
result, err := d.CheckHost(domain)
if err != nil {
t.Fatalf("Cannot check host due to %s", err)
}
if result.IP != nil {
t.Fatalf("SafeSearch is not enabled but there is an answer!")
}
// Enable safesearch and check host
d.SafeSearchEnabled = true
// Let's lookup for safesearch domain
safeDomain, ok := d.SafeSearchDomain(domain)
if !ok {
t.Fatalf("Failed to get safesearch domain for %s", domain)
}
ips, err := net.LookupIP(safeDomain)
if err != nil {
t.Fatalf("Failed to lookup for %s", safeDomain)
}
ip := ips[0]
for _, i := range ips {
if len(i) == net.IPv6len && i.To4() != nil {
ip = i
}
}
result, err = d.CheckHost(domain)
if err != nil {
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
}
if result.IP.String() != ip.String() {
t.Fatalf("Wrong IP for %s safesearch: %s", domain, result.IP.String())
}
// Check cache
cachedValue, isFound, err := getCachedReason(safeSearchCache, domain)
if err != nil {
t.Fatalf("An error occured during cache search for %s: %s", domain, err)
}
if !isFound {
t.Fatalf("Safesearch cache doesn't work for %s!", domain)
}
if cachedValue.IP.String() != ip.String() {
t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String())
}
}
func TestParentalControl(t *testing.T) {
d := NewForTest()
defer d.Destroy()
@@ -866,7 +1019,7 @@ func BenchmarkLotsOfRulesLotsOfHosts(b *testing.B) {
for n := 0; n < b.N; n++ {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
_, _ = hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}
@@ -903,7 +1056,7 @@ func BenchmarkLotsOfRulesLotsOfHostsParallel(b *testing.B) {
for pb.Next() {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
_, _ = hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}

View File

@@ -1,11 +1,16 @@
package dnsfilter
var safeSearchDomains = map[string]string{
"yandex.com": "213.180.193.56",
"yandex.ru": "213.180.193.56",
"yandex.ua": "213.180.193.56",
"yandex.by": "213.180.193.56",
"yandex.kz": "213.180.193.56",
"yandex.com": "213.180.193.56",
"yandex.ru": "213.180.193.56",
"yandex.ua": "213.180.193.56",
"yandex.by": "213.180.193.56",
"yandex.kz": "213.180.193.56",
"www.yandex.com": "213.180.193.56",
"www.yandex.ru": "213.180.193.56",
"www.yandex.ua": "213.180.193.56",
"www.yandex.by": "213.180.193.56",
"www.yandex.kz": "213.180.193.56",
"www.bing.com": "strict.bing.com",

View File

@@ -1,9 +1,11 @@
package dnsforward
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
@@ -35,15 +37,27 @@ const (
//
// The zero Server is empty and ready for use.
type Server struct {
dnsProxy *proxy.Proxy // DNS proxy instance
dnsProxy *proxy.Proxy // DNS proxy instance
dnsFilter *dnsfilter.Dnsfilter // DNS filter instance
queryLog *queryLog // Query log instance
stats *stats // General server statistics
once sync.Once
sync.RWMutex
ServerConfig
}
// NewServer creates a new instance of the dnsforward.Server
// baseDir is the base directory for query logs
func NewServer(baseDir string) *Server {
return &Server{
queryLog: newQueryLog(baseDir),
stats: newStats(),
}
}
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
@@ -57,19 +71,29 @@ type FilteringConfig struct {
dnsfilter.Config `yaml:",inline"`
}
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
type TLSConfig struct {
TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key
}
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
UDPListenAddr *net.UDPAddr // UDP listen address
TCPListenAddr *net.TCPAddr // TCP listen address
Upstreams []upstream.Upstream // Configured upstreams
Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig
TLSConfig
}
// if any of ServerConfig values are zero, then default values from below are used
var defaultValues = ServerConfig{
UDPListenAddr: &net.UDPAddr{Port: 53},
TCPListenAddr: &net.TCPAddr{Port: 53},
FilteringConfig: FilteringConfig{BlockedResponseTTL: 3600},
}
@@ -78,7 +102,7 @@ func init() {
defaultUpstreams := make([]upstream.Upstream, 0)
for _, addr := range defaultDNS {
u, err := upstream.AddressToUpstream(addr, "", DefaultTimeout)
u, err := upstream.AddressToUpstream(addr, upstream.Options{Timeout: DefaultTimeout})
if err == nil {
defaultUpstreams = append(defaultUpstreams, u)
}
@@ -103,26 +127,36 @@ func (s *Server) startInternal(config *ServerConfig) error {
return errors.New("DNS server is already started")
}
if s.queryLog == nil {
s.queryLog = newQueryLog(".")
}
if s.stats == nil {
s.stats = newStats()
}
err := s.initDNSFilter()
if err != nil {
return err
}
log.Printf("Loading stats from querylog")
err = fillStatsFromQueryLog()
log.Tracef("Loading stats from querylog")
err = s.queryLog.fillStatsFromQueryLog(s.stats)
if err != nil {
return errorx.Decorate(err, "failed to load stats from querylog")
}
once.Do(func() {
go periodicQueryLogRotate()
go periodicHourlyTopRotate()
go statsRotator()
// TODO: Think about reworking this, the current approach won't work properly if AG Home is restarted periodically
s.once.Do(func() {
log.Printf("Start DNS server periodic jobs")
go s.queryLog.periodicQueryLogRotate()
go s.queryLog.runningTop.periodicHourlyTopRotate()
go s.stats.statsRotator()
})
// TODO: Add TCPListenAddr
proxyConfig := proxy.Config{
UDPListenAddr: s.UDPListenAddr,
TCPListenAddr: s.TCPListenAddr,
Ratelimit: s.Ratelimit,
RatelimitWhitelist: s.RatelimitWhitelist,
RefuseAny: s.RefuseAny,
@@ -131,10 +165,23 @@ func (s *Server) startInternal(config *ServerConfig) error {
Handler: s.handleDNSRequest,
}
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
proxyConfig.TLSListenAddr = s.TLSListenAddr
keypair, err := tls.X509KeyPair([]byte(s.CertificateChain), []byte(s.PrivateKey))
if err != nil {
return errorx.Decorate(err, "Failed to parse TLS keypair")
}
proxyConfig.TLSConfig = &tls.Config{Certificates: []tls.Certificate{keypair}}
}
if proxyConfig.UDPListenAddr == nil {
proxyConfig.UDPListenAddr = defaultValues.UDPListenAddr
}
if proxyConfig.TCPListenAddr == nil {
proxyConfig.TCPListenAddr = defaultValues.TCPListenAddr
}
if len(proxyConfig.Upstreams) == 0 {
proxyConfig.Upstreams = defaultValues.Upstreams
}
@@ -146,7 +193,7 @@ func (s *Server) startInternal(config *ServerConfig) error {
// Initializes the DNS filter
func (s *Server) initDNSFilter() error {
log.Printf("Creating dnsfilter")
log.Tracef("Creating dnsfilter")
s.dnsFilter = dnsfilter.New(&s.Config)
// add rules only if they are enabled
if s.FilteringEnabled {
@@ -181,17 +228,7 @@ func (s *Server) stopInternal() error {
}
// flush remainder to file
logBufferLock.Lock()
flushBuffer := logBuffer
logBuffer = nil
logBufferLock.Unlock()
err := flushToFile(flushBuffer)
if err != nil {
log.Printf("Saving querylog to file failed: %s", err)
return err
}
return nil
return s.queryLog.flushLogBuffer()
}
// IsRunning returns true if the DNS server is running
@@ -223,6 +260,52 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
return nil
}
// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.RLock()
s.dnsProxy.ServeHTTP(w, r)
s.RUnlock()
}
// GetQueryLog returns a map with the current query log ready to be converted to a JSON
func (s *Server) GetQueryLog() []map[string]interface{} {
s.RLock()
defer s.RUnlock()
return s.queryLog.getQueryLog()
}
// GetStatsTop returns the current stop stats
func (s *Server) GetStatsTop() *StatsTop {
s.RLock()
defer s.RUnlock()
return s.queryLog.runningTop.getStatsTop()
}
// PurgeStats purges current server stats
func (s *Server) PurgeStats() {
s.Lock()
defer s.Unlock()
s.stats.purgeStats()
}
// GetAggregatedStats returns aggregated stats data for the 24 hours
func (s *Server) GetAggregatedStats() map[string]interface{} {
s.RLock()
defer s.RUnlock()
return s.stats.getAggregatedStats()
}
// GetStatsHistory gets stats history aggregated by the specified time unit
// timeUnit is either time.Second, time.Minute, time.Hour, or 24*time.Hour
// start is start of the time range
// end is end of the time range
// returns nil if time unit is not supported
func (s *Server) GetStatsHistory(timeUnit time.Duration, startTime time.Time, endTime time.Time) (map[string]interface{}, error) {
s.RLock()
defer s.RUnlock()
return s.stats.getStatsHistory(timeUnit, startTime, endTime)
}
// handleDNSRequest filters the incoming DNS requests and writes them to the query log
func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
start := time.Now()
@@ -255,7 +338,10 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
if d.Upstream != nil {
upstreamAddr = d.Upstream.Address()
}
logRequest(msg, d.Res, res, elapsed, d.Addr, upstreamAddr)
entry := s.queryLog.logRequest(msg, d.Res, res, elapsed, d.Addr, upstreamAddr)
if entry != nil {
s.stats.incrementCounters(entry)
}
}
return nil
@@ -300,12 +386,12 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
return s.genBlockedHost(m, safeBrowsingBlockHost, d.Upstream)
return s.genBlockedHost(m, safeBrowsingBlockHost, d)
case dnsfilter.FilteredParental:
return s.genBlockedHost(m, parentalBlockHost, d.Upstream)
return s.genBlockedHost(m, parentalBlockHost, d)
default:
if result.Ip != nil {
return s.genARecord(m, result.Ip)
if result.IP != nil {
return s.genARecord(m, result.IP)
}
return s.genNXDomain(m)
@@ -331,22 +417,30 @@ func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
return &resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, upstream upstream.Upstream) *dns.Msg {
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
// look up the hostname, TODO: cache
replReq := dns.Msg{}
replReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)
replReq.RecursionDesired = true
reply, err := upstream.Exchange(&replReq)
newContext := &proxy.DNSContext{
Proto: d.Proto,
Addr: d.Addr,
StartTime: time.Now(),
Req: &replReq,
}
err := s.dnsProxy.Resolve(newContext)
if err != nil {
log.Printf("Couldn't look up replacement host '%s' on upstream %s: %s", newAddr, upstream.Address(), err)
log.Printf("Couldn't look up replacement host '%s': %s", newAddr, err)
return s.genServerFailure(request)
}
resp := dns.Msg{}
resp.SetReply(request)
resp.Authoritative, resp.RecursionAvailable = true, true
if reply != nil {
for _, answer := range reply.Answer {
if newContext.Res != nil {
for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
resp.Answer = append(resp.Answer, answer)
}
@@ -396,5 +490,3 @@ func (s *Server) genSOA(request *dns.Msg) []dns.RR {
}
return []dns.RR{&soa}
}
var once sync.Once

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