Compare commits

..

165 Commits

Author SHA1 Message Date
Andrey Meshkov
00fabb0ecf Merge: + client: add X-DNS-Prefetch-Control meta tag
* commit '073643537612437430c7035cda679a758ca94d13':
  + client: add meta tag to index.html
  + client: add X-DNS-Prefetch-Control meta tag
2019-12-19 12:16:13 +03:00
Ildar Kamalov
0736435376 + client: add meta tag to index.html 2019-12-19 12:13:15 +03:00
Andrey Meshkov
f6976f3c7e Merge: - DNS: set RecursionAvailable flag in response message
* commit '4540a4e94ad204fc1cba9e15b95ce9b684ed2335':
  - DNS: set RecursionAvailable flag in response message
2019-12-19 12:09:32 +03:00
Simon Zolin
4540a4e94a - DNS: set RecursionAvailable flag in response message 2019-12-19 11:52:21 +03:00
Ildar Kamalov
bf410c81ae + client: add X-DNS-Prefetch-Control meta tag 2019-12-19 10:23:04 +03:00
Andrey Meshkov
b54bf94697 Merge: - client: hide dns is starting message by default
* commit '7fade498b910a2492b2e214f0b2a706b51548b34':
  - client: add setDnsRunningStatus action
  - client: save in store dnsStatus even if running false
  - client: hide dns is starting message by default
2019-12-17 22:35:34 +03:00
Artem Baskal
7fade498b9 - client: add setDnsRunningStatus action 2019-12-17 18:54:28 +03:00
Artem Baskal
39640d8190 - client: save in store dnsStatus even if running false 2019-12-17 17:46:59 +03:00
Artem Baskal
242e5e136f - client: hide dns is starting message by default 2019-12-17 16:15:44 +03:00
Simon Zolin
b105f20837 Merge: - DNS: fix slow response to /status and /access/list requests
Close #1264

* commit '8521635f63e9570a4e75033533dec8180e7f130a':
  - DNS: fix slow response to /status and /access/list requests
2019-12-17 15:11:48 +03:00
Simon Zolin
8521635f63 - DNS: fix slow response to /status and /access/list requests 2019-12-17 13:09:03 +03:00
Simon Zolin
04de9d0f7b Merge: - DNS: "custom_ip" blocking mode didn't work after app restart
Close #1262

Squashed commit of the following:

commit bacd683ef5b52e275323a3c07b370ca08702403e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 17:00:49 2019 +0300

    fix

commit 3d4f9626460de3e13a621f2b8e535e9e0939e2bb
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 16:54:23 2019 +0300

    fix

commit bf924bf90e9b705883bec88f8d7af11c39c1f322
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 16:45:41 2019 +0300

    add test

commit 43338ea3645a025d69dd838bc732344255960bed
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 16:07:51 2019 +0300

    - DNS: "custom_ip" blocking mode didn't work after app restart

commit 220f32e713a95d2c67355c61e419dd09df9d42b2
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 15:46:01 2019 +0300

    - first run: fix panic on stop in case initialization didn't complete

    e.g. when Stats module can't be initialized because of incompatible file system
2019-12-16 17:04:30 +03:00
Simon Zolin
6a2430b799 Merge: - clients: IPv6 address matching didn't work
Close #1261

Squashed commit of the following:

commit acc39ea6c0d88cb9d2b07837e89db2c170263891
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 12:29:33 2019 +0300

    minor

commit 0d2ef3d53185d5ca17797e2ac20f0efc1498a53c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 12:13:17 2019 +0300

    add link to GH

commit 0da754b1751057968780b457a2f490f4148275a8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 16 11:53:42 2019 +0300

    - clients: IPv6 address matching didn't work
2019-12-16 12:36:52 +03:00
Simon Zolin
d9ee9b88d6 Merge: Revert "Merge: + DNS: TLS handshake: terminate handshake on bad SNI"
* commit 'b00a789ca34008dfe9e4cfdb0b2d4d63874957ff':
  Revert "Merge: + DNS: TLS handshake: terminate handshake on bad SNI"
2019-12-13 17:42:57 +03:00
Simon Zolin
864c91e524 Merge: - DNS: fix security checks via PC/SB services
Squashed commit of the following:

commit e73bc282d77a11c923a86166035f1b44427d7066
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Dec 13 17:17:36 2019 +0300

    fix

commit f8b5c174816c6fd57fb3930cc465318f468fc8ff
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Dec 13 17:03:13 2019 +0300

    fix

commit 9d5483a2fb89a172218547b5ee356e7122dca609
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Dec 13 16:54:30 2019 +0300

    - fix security checks via PC/SB services
2019-12-13 17:42:01 +03:00
Simon Zolin
b00a789ca3 Revert "Merge: + DNS: TLS handshake: terminate handshake on bad SNI"
This reverts commit c8c76ae12b.
2019-12-13 17:38:17 +03:00
Simon Zolin
42790bf083 Merge: * client: correction on message toast
* commit '4942d0c39f2b078ba25993151e7ee59fa0e969b8':
  * client: correction on message toast
2019-12-13 14:02:13 +03:00
Fresnel Vincent
4942d0c39f * client: correction on message toast
fix #1255
2019-12-13 13:50:42 +03:00
Andrey Meshkov
af7f51d9b9 Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home 2019-12-13 13:31:13 +03:00
Andrey Meshkov
3d280c5d92 *: run travis in AG repo only 2019-12-13 13:31:10 +03:00
Simon Zolin
c8c76ae12b Merge: + DNS: TLS handshake: terminate handshake on bad SNI
Close #1014

Squashed commit of the following:

commit 759248efc0587ff2f288996c47739e602c557a76
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 19:26:46 2019 +0300

    support empty ServerName

commit 68afecd5eca5ae66262b12dcb414b50efe88dc02
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 14:40:22 2019 +0300

    + DNS: TLS handshake: terminate handshake on bad SNI
2019-12-13 13:06:37 +03:00
Simon Zolin
aa202277fb Merge: * update translations
Close #1250

Squashed commit of the following:

commit e722a3e3b4b1049c5df22b3f11b5826992d3cc6c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 18:27:51 2019 +0300

    update ru

commit 086e722a4dc1922ac7b8aff537050689900a8bd0
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 18:15:15 2019 +0300

    fix cloudflare -> quad9, update es

commit 86c235936d2f86e536f45fc475f29b43eb695e3d
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 18:01:20 2019 +0300

    * update es, ru, sr-cs

commit 2b6b510b998b7e7b2f4c3fa5d7af57dc5413a611
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 17:43:05 2019 +0300

    * update 'no'

commit 1de6215aa90374ee147bbd0a685643232e98e4c8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 17:41:37 2019 +0300

    * use dnsproxy v0.23.2

commit c545a1bf7e4e706b45b4670b804a50bf8d75bae3
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 17:36:37 2019 +0300

    update ru

commit c22eec9bcfad67284e21cfc4e6b06fb63a64d25d
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Dec 12 17:22:03 2019 +0300

    * update translations;  add sr-cs, fa, hr
2019-12-12 18:42:51 +03:00
Simon Zolin
9cc09274f1 Merge: - DNS: fix deadlock
Close #1251

* commit 'ef57f7e192fdd4fbbfe25dc1c1e61d52e9f0b6ee':
  - DNS: fix race in WriteDiskConfig()
  - DNS: fix deadlock in Server.ServeHTTP()
2019-12-12 15:08:19 +03:00
Simon Zolin
ef57f7e192 - DNS: fix race in WriteDiskConfig() 2019-12-12 15:04:29 +03:00
Simon Zolin
000e842f7b - DNS: fix deadlock in Server.ServeHTTP()
s.RLock() is called again in filterResponse() while another thread
 holds s.Lock()
2019-12-12 15:00:10 +03:00
Ildar Kamalov
e85fdd7f09 Merge: - client: fix request count in clients table
* commit '8c8deb3d3db72a2f091156d3ec10447f0e6c062c':
  - client: fix request count in clients table
2019-12-12 13:46:44 +03:00
Ildar Kamalov
8c8deb3d3d - client: fix request count in clients table 2019-12-12 11:33:02 +03:00
Simon Zolin
c9ccc53282 Merge: * set BlockingMode: "null_ip" by default; minor improvements
Squashed commit of the following:

commit 653544b98dc4d1b9a74e1509d0e6104b71bcdcb3
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 17:34:41 2019 +0300

    * DNS reconfigure: protect against delayed socket fd close

commit 9e650f37dee7f771bf1d9d714c35f0a81788aa16
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 15:28:33 2019 +0300

    - fix race on startup

commit 878fdb8fc4ebbc6fab683a65f5e4298e64c2073e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 15:11:21 2019 +0300

    * travis: don't run tests

commit 1c4ab60684ee22d55e6d2a3350c0f24d9844255c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 14:56:28 2019 +0300

    * travis: 'release.sh' and then run tests

commit e1f644b8d9a1f3b46990cdfb1b75fd81b3a49d33
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 11 14:52:59 2019 +0300

    * set BlockingMode: "null_ip" by default
2019-12-11 17:54:34 +03:00
Andrey Meshkov
daf17f16f2 -(home): fix whois info on the dashboard 2019-12-11 14:31:38 +03:00
Simon Zolin
0a66913b4d Merge: * use upstream servers directly for the internal DNS resolver
Close #1212

* Server.Start(config *ServerConfig) -> Start()
+ Server.Prepare(config *ServerConfig)
+ Server.Resolve(host string)
+ Server.Exchange()
* rDNS: use internal DNS resolver
- clients: fix race in WriteDiskConfig()
- fix race: move 'clients' object from 'configuration' to 'HomeContext'
    Go race detector didn't like our 'clients' object in 'configuration'.
+ add AGH startup test
    . Create a configuration file
    . Start AGH instance
    . Check Web server
    . Check DNS server
    . Wait until the filters are downloaded
    . Stop and cleanup
* move module objects from config.* to Context.*
* don't call log.SetLevel() if not necessary
    This helps to avoid Go race detector's warning
* ci.sh: 'make' and then run tests

Squashed commit of the following:

commit 86500c7f749307f37af4cc8c2a1066f679d0cfad
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 18:08:53 2019 +0300

    minor

commit 6e6abb9dca3cd250c458bec23aa30d2250a9eb40
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 18:08:31 2019 +0300

    * ci.sh: 'make' and then run tests

commit 114192eefea6800e565ba9ab238202c006516c27
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 17:50:04 2019 +0300

    fix

commit d426deea7f02cdfd4c7217a38c59e51251956a0f
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 17:46:33 2019 +0300

    tests

commit 7b350edf03027895b4e43dee908d0155a9b0ac9b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:56:12 2019 +0300

    fix test

commit 2f5f116873bbbfdd4bb7f82a596f9e1f5c2bcfd8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:48:56 2019 +0300

    fix tests

commit 3fbdc77f9c34726e2295185279444983652d559e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:45:00 2019 +0300

    linter

commit 9da0b6965a2b6863bcd552fa83a4de2866600bb8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:33:23 2019 +0300

    * config.dnsctx.whois -> Context.whois

commit c71ebdbdf6efd88c877b2f243c69d3bc00a997d7
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:31:08 2019 +0300

    * don't call log.SetLevel() if not necessary

    This helps to avoid Go race detector's warning

commit 0f250220133cefdcb0843a50000cb932802b8324
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 15:28:19 2019 +0300

    * rdns: refactor

commit c460d8c9414940dac852e390b6c1b4d4fb38dff9
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 14:08:08 2019 +0300

    Revert: * stats: serialize access to 'limit'

    Use 'conf *Config' and update it atomically, as in querylog module.
    (Note: Race detector still doesn't like it)

commit 488bcb884971276de0d5629384b29e22c59ee7e6
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 13:50:23 2019 +0300

    * config.dnsFilter -> Context.dnsFilter

commit 86c0a6827a450414b50acec7ebfc5220d13b81e4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 13:45:05 2019 +0300

    * config.dnsServer -> Context.dnsServer

commit ee35ef095ccaabc89e3de0ef52c9b5ed56b36873
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 13:42:10 2019 +0300

    * config.dhcpServer -> Context.dhcpServer

commit 1537001cd211099d5fad01696c0b806ae5d257b1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 13:39:45 2019 +0300

    * config.queryLog -> Context.queryLog

commit e5955fe4ff1ef6f41763461b37b502ea25a3d04c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Dec 10 13:03:18 2019 +0300

    * config.httpsServer -> Context.httpsServer

commit 6153c10a9ac173e159d1f05e0db1512579b9203c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 20:12:24 2019 +0300

    * config.httpServer -> Context.httpServer

commit abd021fb94039015cd45c97614e8b78d4694f956
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 20:08:05 2019 +0300

    * stats: serialize access to 'limit'

commit 38c2decfd87c712100edcabe62a6d4518719cb53
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 19:57:04 2019 +0300

    * config.stats -> Context.stats

commit 6caf8965ad44db9dce9a7a5103aa8fa305ad9a06
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 19:45:23 2019 +0300

    fix Restart()

... and 6 more commits
2019-12-11 12:38:58 +03:00
Artem Baskal
fe357d04f7 Merge: - client: add message when dns server is starting up
Close #1186

* commit '1e429df3bcb8ea2fd3a457c026cb8c8a7131e504':
  - client: add message when dns server is starting up
2019-12-10 18:30:51 +03:00
Artem Baskal
1e429df3bc - client: add message when dns server is starting up 2019-12-10 17:48:42 +03:00
Simon Zolin
2b14a043a9 Merge: + DNS: new settings (ratelimit, blocking mode, edns_client_subnet)
Close #1091 Close #1154 Close #1022

* commit '97e77cab643d6784067ce97c0f03ec3e4612c2c9':
  + client: handle EDNS Client Subnet setting
  + dns: add "edns_client_subnet" setting
  + client: handle DNS config
  * DNS: remove /enable_protection and /disable_protection
  + openapi: /dns_info, /dns_config
  * /control/set_upstreams_config: allow empty upstream list
  + dns: support blocking_mode=custom_ip
  + DNS: Get/Set DNS general settings
2019-12-10 16:10:31 +03:00
Ildar Kamalov
97e77cab64 + client: handle EDNS Client Subnet setting 2019-12-10 16:01:18 +03:00
Simon Zolin
19a94bf789 + dns: add "edns_client_subnet" setting 2019-12-10 16:01:17 +03:00
Artem Baskal
197d07f32b + client: handle DNS config 2019-12-10 16:01:17 +03:00
Simon Zolin
87bb773d3e * DNS: remove /enable_protection and /disable_protection 2019-12-10 16:01:17 +03:00
Simon Zolin
3b13c031a3 + openapi: /dns_info, /dns_config 2019-12-10 16:01:17 +03:00
Simon Zolin
1b3122dd35 * /control/set_upstreams_config: allow empty upstream list 2019-12-10 16:01:17 +03:00
Simon Zolin
4f4da3397c + dns: support blocking_mode=custom_ip 2019-12-10 16:01:16 +03:00
Simon Zolin
26ccee47b5 + DNS: Get/Set DNS general settings
GET /control/dns_info
POST /control/dns_config
2019-12-10 16:01:16 +03:00
Ildar Kamalov
92141e03c4 Merge: - client: fix request count in clients table
* commit '289c6f8c731f616c73eac2739b572c13eb245680':
  - client: fix request count in clients table
2019-12-10 15:55:38 +03:00
Ildar Kamalov
289c6f8c73 - client: fix request count in clients table 2019-12-10 15:22:40 +03:00
Simon Zolin
40763c903b Merge: * stats: handle "invalid argument" error from "bbolt"
Close #1188

Squashed commit of the following:

commit 70355154bb49ae7df57e79ed1405754682f5d57f
Merge: 331650e6 d1c47c0d
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 14:09:59 2019 +0300

    Merge remote-tracking branch 'origin/master' into 1188-bolt-err

commit 331650e60d5869b7c706b539f04c1a856cd9f254
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Dec 9 13:57:14 2019 +0300

    minor

commit 07130a0f1f2e88cab30a88d899e8d817096ac35f
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Dec 4 13:50:29 2019 +0300

    * stats: handle "invalid argument" error from "bbolt"
2019-12-09 14:13:39 +03:00
Artem Baskal
d1c47c0dbb Merge: + client: add norwegian language
Closes #1174

* commit '4f0975bab5db189a192aeb07c7e8f6cebc101f34':
  + client: add norwegian language
2019-12-05 16:36:51 +03:00
Artem Baskal
4f0975bab5 + client: add norwegian language 2019-12-05 14:56:52 +03:00
Simon Zolin
9fd27b9add Merge: + Clients: use per-client DNS servers
Close #821

* commit '6684a120ac9909140e4ecbe674e09c8f24e87ac4':
  + client: handle upstream DNS servers for clients
  * openapi: add "upstreams" to Client struct
  + use per-client DNS servers
2019-12-05 13:22:50 +03:00
Ildar Kamalov
6684a120ac + client: handle upstream DNS servers for clients 2019-12-05 13:19:27 +03:00
Simon Zolin
72ec4d7d7e * openapi: add "upstreams" to Client struct 2019-12-05 13:16:41 +03:00
Simon Zolin
7313c3bc53 + use per-client DNS servers 2019-12-05 13:16:41 +03:00
Simon Zolin
b6b26c0681 Merge: - rDNS: support multiple Answer records
Close #1230

* commit '56a0c9684bfdb8bebbeea5eb94192a215d8d45a1':
  - rDNS: support multiple Answer records
2019-12-05 13:14:57 +03:00
Simon Zolin
56a0c9684b - rDNS: support multiple Answer records 2019-12-05 13:06:52 +03:00
Andrey Meshkov
47cc025504 Merge: fix #1211 * use dnsproxy v0.21.0 and urlfilter v0.7.0
* commit '8dbdf49c32d66288dece97b84a6b70b05e5ff43e':
  * use dnsproxy v0.22.0 and urlfilter v0.7.0
2019-12-04 20:44:40 +03:00
Simon Zolin
89cc1715d9 Merge: - DHCP: normalize leases on startup (prioritize static leases over dynamic)
Close #1005

* commit '906f26d7d276c181960048a2c059d5b141e71f0a':
  - DHCP: normalize leases on startup (prioritize static leases over dynamic)
2019-12-04 19:43:04 +03:00
Simon Zolin
8dbdf49c32 * use dnsproxy v0.22.0 and urlfilter v0.7.0 2019-12-03 20:55:45 +03:00
Simon Zolin
906f26d7d2 - DHCP: normalize leases on startup (prioritize static leases over dynamic) 2019-12-03 20:43:22 +03:00
Simon Zolin
07a9c3bd45 Merge: + dnsforward: match CNAME with filtering rules
Close #1185

* commit '8cb4d128f5392646eb60229717b09f8398897075':
  * openapi: get /querylog: add "original_answer", "service_name"
  * doc: filtering logic with a diagram
  + client: handle blocked by response in query log
  + dnsforward: match CNAME with filtering rules
2019-12-03 17:09:30 +03:00
Simon Zolin
8cb4d128f5 * openapi: get /querylog: add "original_answer", "service_name" 2019-12-03 17:01:26 +03:00
Simon Zolin
7c0b2d8ede * doc: filtering logic with a diagram 2019-12-03 17:01:26 +03:00
Ildar Kamalov
e8885dbf3e + client: handle blocked by response in query log 2019-12-03 17:01:26 +03:00
Simon Zolin
e7727e9f63 + dnsforward: match CNAME with filtering rules
+ GET /control/querylog: add "cname_match" field

* querylog: Add() now receives an object with parameters
2019-12-03 17:01:26 +03:00
Ildar Kamalov
4d7984cb8b Merge: Update blocked_services.go Snapchat Rules
Closes #1222

* commit '9c2e5c3f51fa7fdc5f5e6d89a73c1456a8df49d0':
  Update blocked_services.go Snapchat Rules
2019-12-03 13:35:09 +03:00
Simon Zolin
acd8da6f0a Merge: * DNS: use Quad9 as default server
Close #1116

* commit '82d0c3047b857651d351a7acd3f2e8bb262dfb4d':
  + client: change DNS settings text and examples to Quad9
  * DNS: use Quad9 as default server
2019-12-02 15:57:46 +03:00
Simon Zolin
48c70aefaa Merge: + QueryLog: new setting "dns.querylog_memsize"
Close #958

* commit '7e45c2fc2428679ac7454c5244b0130901449324':
  + new setting "dns.querylog_memsize"
2019-12-02 15:57:17 +03:00
Simon Zolin
7e45c2fc24 + new setting "dns.querylog_memsize"
* set 1000 entries by default (not 5000)
2019-12-02 15:46:03 +03:00
Ildar Kamalov
82d0c3047b + client: change DNS settings text and examples to Quad9 2019-12-02 15:40:54 +03:00
Simon Zolin
d6d0d53761 * DNS: use Quad9 as default server 2019-12-02 15:40:54 +03:00
Simon Zolin
31ffae7717 Merge: * dnsforward: major refactoring
* commit 'f579c23bc932e893ffa960abe769750fe657beff':
  * minor fixes
  * dnsforward: refactor code for default DNS servers logic
  * update tests
  * dnsforward: move access settings and web handlers
  + dnsforward: refactor
2019-12-02 15:38:04 +03:00
Simon Zolin
f579c23bc9 * minor fixes 2019-12-02 15:25:11 +03:00
Simon Zolin
9b8cccdfcf * dnsforward: refactor code for default DNS servers logic 2019-12-02 14:58:17 +03:00
Simon Zolin
8bf75b54a4 * update tests 2019-12-02 14:58:17 +03:00
Simon Zolin
19a1c03d3b * dnsforward: move access settings and web handlers 2019-12-02 14:58:17 +03:00
Simon Zolin
7bb32eae3d + dnsforward: refactor
+ dnsforward: own HTTP handlers
* dnsforward: no DNS reload on ProtectionEnabled setting change
* dnsforward: move QueryLog* settings out
* dnsforward: move dnsfilter settings out
* clients,i18n: no DNS reload on settings change
2019-12-02 14:58:17 +03:00
Simon Zolin
73d17ffa81 Merge: + DHCP: Reset DHCP configuration; Refactor
Close #1024

* commit 'a6926169815e03d00f9cfac0417da6f797d539ae':
  - clients: fix tests
  * clients: refactor
  + openapi: /dhcp/reset
  + client: handle DHCP reset
  + POST /control/dhcp/reset: Reset DHCP configuration
  * dhcp: move HTTP handlers to dhcpd/
  * dhcp,clients: DHCP server module is passed to Clients module during initialization.
2019-12-02 12:56:30 +03:00
Cybo1927
9c2e5c3f51 Update blocked_services.go Snapchat Rules
Snapchat still works on Android if that's not blocked
2019-11-29 16:55:27 +00:00
Simon Zolin
a692616981 - clients: fix tests 2019-11-29 17:35:26 +03:00
Simon Zolin
e9cb8666ce * clients: refactor 2019-11-29 17:11:07 +03:00
Simon Zolin
90ce70225f + openapi: /dhcp/reset 2019-11-29 17:11:07 +03:00
Ildar Kamalov
a4dedacf43 + client: handle DHCP reset 2019-11-29 17:11:07 +03:00
Simon Zolin
df92941ae0 + POST /control/dhcp/reset: Reset DHCP configuration 2019-11-29 17:11:07 +03:00
Simon Zolin
477a4bbf54 * dhcp: move HTTP handlers to dhcpd/ 2019-11-29 17:11:07 +03:00
Simon Zolin
149fcc0f2d * dhcp,clients: DHCP server module is passed to Clients module during initialization. 2019-11-29 17:11:07 +03:00
Simon Zolin
127a68a39f Merge: + Clients: multiple IP, CIDR, MAC
Close #809

* commit 'f8202a74bd55b97cdd95c33d8f8ed97412ba43b1':
  + client: handle clients find
  + client: add multiple fields client form
  * minor
  * openapi: update 'Client' object; add /clients/find
  * config: upgrade scheme to v6
  * clients: multiple IP, CIDR, MAC addresses
2019-11-29 17:05:00 +03:00
Ildar Kamalov
f8202a74bd + client: handle clients find 2019-11-29 16:59:20 +03:00
Ildar Kamalov
a6d6e9ec9e + client: add multiple fields client form 2019-11-29 16:53:21 +03:00
Simon Zolin
fd26af2677 * minor 2019-11-29 16:53:21 +03:00
Simon Zolin
1fc70cbc08 * openapi: update 'Client' object; add /clients/find 2019-11-29 16:53:21 +03:00
Simon Zolin
317a9cc49e * config: upgrade scheme to v6 2019-11-29 16:53:05 +03:00
Simon Zolin
71ce0c6da9 * clients: multiple IP, CIDR, MAC addresses
+ /clients/find
* clients: move code for config read/write
* clients: move HTTP handlers
2019-11-29 16:52:32 +03:00
Artem Baskal
db703283ba Merge pull request #431 in DNS/adguard-home from fix-1075 to master
* commit '49e800727b2e866fa1f9d429bd1c0c17ce8466b3':
  - client: allow add ip-v6 to exceptions
2019-11-27 18:50:00 +03:00
Artem Baskal
49e800727b - client: allow add ip-v6 to exceptions 2019-11-27 18:45:30 +03:00
Artem Baskal
d26fed1901 Merge: - client: fix table size and add pagination
Close #1197

* commit 'f446186db146b74cc6af3d29cf53ff69adc26e4f':
  - client: fix table pagination
  - client: fix table size and add pagination
2019-11-27 13:06:11 +03:00
Artem Baskal
f446186db1 - client: fix table pagination 2019-11-26 20:50:02 +03:00
Artem Baskal
4751528d69 - client: fix table size and add pagination 2019-11-26 19:24:22 +03:00
Simon Zolin
48c0b487e3 Merge: * auth: rename "session" cookie to "agh_session"
Close #1196

* commit 'b3614ba62fcde8ff3e3b6e708095f84a230aa2f2':
  * auth: rename "session" cookie to "agh_session"
2019-11-26 19:21:34 +03:00
Ildar Kamalov
7a665ff8f0 Merge: - client: fix placeholder for table pages
Closes #1166

* commit '38c7e732a6af190f92d824a6af53aea60aab5146':
  - client: fix placeholder for table pages
2019-11-26 15:30:00 +03:00
Ildar Kamalov
38c7e732a6 - client: fix placeholder for table pages 2019-11-26 15:16:52 +03:00
Ildar Kamalov
bdffd8534c Merge: Update blocked_services.goTikTok Rules
Closes #1192

* commit 'f19d8c24c3e3a8d2934affc5690cc72c044a0a3f':
  Update blocked_services.goTikTok Rules
2019-11-26 13:48:58 +03:00
Ildar Kamalov
757e41ce53 Merge: Update blocked_services.go Facebook, Messenger, and Instagram Rules
Closes #1190

* commit 'e0be4e8801307bebce9e19057a1a8e6fb1a658c7':
  Update blocked_services.go Facebook, Messenger, and Instagram Rules
2019-11-26 13:48:31 +03:00
Cybo1927
f19d8c24c3 Update blocked_services.goTikTok Rules 2019-11-26 12:01:09 +03:00
Cybo1927
e0be4e8801 Update blocked_services.go Facebook, Messenger, and Instagram Rules 2019-11-26 11:17:22 +03:00
Ildar Kamalov
713613e3b8 Merge: Update blocked_services.go Snapchat Rules
Closes #1189

* commit 'a78156a7cb259f3cb0a78b7466e09ee4b99f2356':
  Update blocked_services.go Snapchat Rules
2019-11-26 11:12:19 +03:00
Ildar Kamalov
af3096e23b Merge: blocked services: add new domain for youtube
Closes #1149

* commit '9675b3776ee50d8df4ce8a875f9b70b22bb8f8ee':
  blocked services: add new domain for youtube
2019-11-26 11:10:19 +03:00
Simon Zolin
b3614ba62f * auth: rename "session" cookie to "agh_session"
for compatibility with other HTTP services on the same host
2019-11-25 15:48:14 +03:00
Simon Zolin
a0fe1e84c0 Merge: * use Go v1.13
Close #1041

* commit 'dc2c68b1e4ff625deb44850a84282cdce56d1cd5':
  * use Go v1.13
2019-11-25 15:29:03 +03:00
Simon Zolin
2e5264652e Merge: - querylog: remove time counter (fix zero div exception)
Close #1194

* commit 'a31116635e605d199887e83a98d27f0fe01291c1':
  * querylog: delete code for time counter
  - querylog: remove time counter (fix zero div exception)
2019-11-25 15:25:55 +03:00
Simon Zolin
a31116635e * querylog: delete code for time counter 2019-11-25 15:20:56 +03:00
Simon Zolin
dc2c68b1e4 * use Go v1.13 2019-11-25 14:10:28 +03:00
Simon Zolin
305df63054 - querylog: remove time counter (fix zero div exception) 2019-11-25 13:40:27 +03:00
Cybo1927
a78156a7cb Update blocked_services.go Snapchat Rules 2019-11-22 14:54:14 +03:00
ArchiveBase
9675b3776e blocked services: add new domain for youtube 2019-11-22 14:50:38 +03:00
Andrey Meshkov
0a09c7eb4d *(global): travis-ci.org -> travis-ci.com 2019-11-21 19:11:02 +03:00
Simon Zolin
6fdffc1203 Merge: Add cloudflare to blocked services
Close #1155

* commit '36636867e18baf54c54a1a36f9d501a0f8bbeea4':
  + blocked_services: add cloudflare
  + client: add cloudflare icon
2019-11-21 11:48:07 +03:00
ArchiveBase
36636867e1 + blocked_services: add cloudflare 2019-11-21 11:40:03 +03:00
ArchiveBase
c93cdd106e + client: add cloudflare icon 2019-11-21 11:36:58 +03:00
Andrey Meshkov
ee41c18d53 Merge: #1092 * /control/querylog_config: support optional parameters
* commit '7c81efcbcba9f5c072ed03d37490cf5e7dd33f89':
  * go.mod: use bbolt from github
  * /control/querylog_config: support optional parameters
2019-11-20 20:28:28 +03:00
Simon Zolin
7c81efcbcb * go.mod: use bbolt from github 2019-11-20 20:14:28 +03:00
Simon Zolin
81e8bbe63c * /control/querylog_config: support optional parameters 2019-11-20 20:12:46 +03:00
Simon Zolin
0b7ac93076 Merge: * blocked_services: update hosts for whatsapp, facebook, youtube, tiktok
Close #1141

* commit '15f6334748ccae9075124a15d0a79987effde990':
  * blocked_services: update hosts for whatsapp, facebook, youtube, tiktok
2019-11-20 20:03:48 +03:00
Cybo1927
15f6334748 * blocked_services: update hosts for whatsapp, facebook, youtube, tiktok 2019-11-20 19:57:38 +03:00
Ildar Kamalov
044a457579 Merge pull request #412 in DNS/adguard-home from add-origin-to-blocked-services to master
* commit '79ea15dce11e96720259cf78db74752fb0cd39d0':
  + client: add origin icon
  + blocked_services: add origin
2019-11-20 18:59:14 +03:00
ArchiveBase
79ea15dce1 + client: add origin icon 2019-11-20 18:55:34 +03:00
ArchiveBase
ca6048f667 + blocked_services: add origin 2019-11-20 18:54:45 +03:00
Ildar Kamalov
fa45f8a256 Merge: Add amazon to blocked services
* commit 'b1c7497d0552b4abd796e82a166d55faa90a030f':
  + client: add ebay icon
  + blocked_services: add ebay
  + client: add amazon icon
  + blocked_services: add amazon
2019-11-20 18:54:13 +03:00
Ildar Kamalov
e7cf8f222d Merge: Add reddit to blocked services
* commit '7c10cefdf9a3d421de2d7e88cfd4e647baf31e8b':
  + client: add reddit icon
  + blocked_services: add reddit
2019-11-20 18:53:35 +03:00
Simon Zolin
2b0ababdc9 Merge: + safesearch: add Pixabay
Close #1159

* commit 'fab63f167ad44f95749f0a779f8805bb39c7d90a':
  + safesearch: add Pixabay
2019-11-20 15:16:28 +03:00
Simon Zolin
d4a3a01f30 Merge: + dnsfilter: use AG DNS server for SB/PC services
Close #525

* commit '7f69848084a68cdf1e227a555454991295fc5866':
  + dnsfilter: use AG DNS server for SB/PC services
2019-11-20 15:09:51 +03:00
Simon Zolin
1b7ef0e4f4 Merge: + Auth: add "web_session_ttl" setting; improve logging
Close #1006

* commit 'b03b36e47e1f230567fce0936ee1285b6861350a':
  * auth: improve logging
  + config: "web_session_ttl" setting
2019-11-20 14:58:20 +03:00
Simon Zolin
b03b36e47e * auth: improve logging
Write info log messages for login attempts (both successful and not)
2019-11-20 13:15:12 +03:00
Simon Zolin
c9a6e4e018 + config: "web_session_ttl" setting 2019-11-20 13:15:08 +03:00
ArchiveBase
b1c7497d05 + client: add ebay icon 2019-11-20 10:28:57 +03:00
ArchiveBase
c49f62cfe6 + blocked_services: add ebay 2019-11-20 10:28:57 +03:00
ArchiveBase
d468daac76 + client: add amazon icon 2019-11-20 10:28:57 +03:00
ArchiveBase
4a59d5a116 + blocked_services: add amazon 2019-11-20 10:28:56 +03:00
ArchiveBase
7c10cefdf9 + client: add reddit icon 2019-11-20 10:24:42 +03:00
ArchiveBase
dbf7a083cb + blocked_services: add reddit 2019-11-20 10:24:41 +03:00
Simon Zolin
0d4dce5c79 Merge: + querylog: preserve searching compatibility with the previous version
* commit 'a7742a366511e272a8c24dc5fcd1a62759c2bacb':
  - querylog: fix linter issue
  + querylog: preserve searching compatibility with the previous version
2019-11-19 17:21:12 +03:00
Simon Zolin
a7742a3665 - querylog: fix linter issue 2019-11-19 17:13:12 +03:00
Simon Zolin
12f4ebc6a5 + querylog: preserve searching compatibility with the previous version 2019-11-19 17:09:54 +03:00
Simon Zolin
6ae0a0eb0b Merge: * openapi: version: '0.99.3'
* commit 'c7e4ecd85cd9f3991bb5f3a001c9b3ae18637527':
  * openapi: version: '0.99.3'
2019-11-19 15:37:11 +03:00
Simon Zolin
c7e4ecd85c * openapi: version: '0.99.3' 2019-11-19 15:26:46 +03:00
Simon Zolin
2ac6e48535 Merge: Querylog: speed up, change format, robust search
Close #1099 Close #1094

* commit '62c8664fd75439b7597d935992c380f9c0675660':
  + client: load additional search results
  + client: separate filters from the table component
  + client: hide page size option and page info
  + client: use oldest param
  * openapi: update 'QueryLog'
  * querylog: add more tests
  * QueryLog.Add() now receives net.IP, not net.Addr
  * querylog: major refactor: change on-disk format and API
2019-11-19 15:21:42 +03:00
Ildar Kamalov
62c8664fd7 + client: load additional search results 2019-11-19 15:14:26 +03:00
Ildar Kamalov
e243e69a6e + client: separate filters from the table component 2019-11-19 15:12:34 +03:00
Ildar Kamalov
6b64d393bd + client: hide page size option and page info 2019-11-19 15:12:34 +03:00
Ildar Kamalov
941b6c5976 + client: use oldest param 2019-11-19 15:12:34 +03:00
Simon Zolin
33093de6aa * openapi: update 'QueryLog' 2019-11-19 15:12:34 +03:00
Simon Zolin
68cd7976b7 * querylog: add more tests 2019-11-19 15:09:53 +03:00
Simon Zolin
0cd6781a9a * QueryLog.Add() now receives net.IP, not net.Addr 2019-11-19 15:09:53 +03:00
Simon Zolin
2f5d6593f2 * querylog: major refactor: change on-disk format and API
speed up decoding
speed up search
compatible with previous format (when not searching)
2019-11-19 15:08:51 +03:00
Simon Zolin
a65f983aac Merge: * querylog: don't return entries without Question data
Close #1143

* commit '0d4e95b36d02e8aadfe6cbdd87d357efba6f02e8':
  * querylog: don't return entries without Question data
2019-11-12 18:31:07 +03:00
Simon Zolin
0d4e95b36d * querylog: don't return entries without Question data 2019-11-12 18:14:33 +03:00
Ildar Kamalov
359cab5290 Merge pull request #384 in DNS/adguard-home from 1123-disable-encryption-sources to master
* commit '4ae4cd07232b1e1861b435d2426ebe951061465b':
  Bind encryption source disabled state to form
2019-11-12 14:30:41 +03:00
Simon Zolin
ec5c5e8109 - Stats: fix crash
Close #1170

* commit 'f64868472aae50ed8203ef4d3ec9de7a7cf96fd9':
  - stats: fix read-write race
  * minor
2019-11-11 18:04:01 +03:00
Simon Zolin
f64868472a - stats: fix read-write race
* periodicFlush() operation doesn't result in an inconsistent state at any time
* stats readers use the last unit ID properly, excluding the possibility
 when unit ID could be changed, but this unit isn't yet created
2019-11-11 16:18:20 +03:00
Simon Zolin
2a6e9f3c11 * minor 2019-11-11 16:13:03 +03:00
Simon Zolin
fab63f167a + safesearch: add Pixabay 2019-11-08 13:07:25 +03:00
Simon Zolin
7f69848084 + dnsfilter: use AG DNS server for SB/PC services
* move SS/SB/PC services to security.go
* remove old useless code (HTTP client)
2019-11-08 10:38:12 +03:00
Simon Zolin
2f1e631c66 Merge: - dns rewrites: CNAME record didn't work
Close #1156

* commit '090f5498331844ad6d5a8dc975c1d0511b47565a':
  - dns rewrites: CNAME record didn't work
2019-11-07 16:33:07 +03:00
Simon Zolin
6b76a2c9f7 Merge: * blocked-services: youtube: add "youtubei.googleapis.com"
Close #1122

* commit '59720467da42c8520e5d25b2c3368728a61d6bf6':
  * blocked-services: youtube: add "youtubei.googleapis.com"
2019-11-07 15:30:23 +03:00
Simon Zolin
090f549833 - dns rewrites: CNAME record didn't work 2019-11-07 15:27:39 +03:00
Simon Zolin
59720467da * blocked-services: youtube: add "youtubei.googleapis.com" 2019-11-07 15:16:47 +03:00
Simon Zolin
4d32d42ba2 Merge: * whois,rdns: use 1 hour cache TTL
Close #1157

* commit 'abd251c5c167fd33c047af8af39e8b8e2d61fd85':
  * whois,rdns: use 1 hour cache TTL
2019-11-07 14:06:06 +03:00
Simon Zolin
abd251c5c1 * whois,rdns: use 1 hour cache TTL 2019-11-07 14:02:34 +03:00
Jaime Martínez Rincón
4ae4cd0723 Bind encryption source disabled state to form 2019-10-25 22:46:08 +02:00
130 changed files with 7820 additions and 3309 deletions

View File

@@ -1,8 +1,9 @@
if: repo = AdguardTeam/AdGuardHome
language: go
sudo: false
go:
- 1.12.x
- 1.13.x
os:
- linux
- osx
@@ -33,17 +34,16 @@ notifications:
matrix:
include:
# Release build configuration
- if: repo = AdguardTeam/AdGuardHome
- name: release
go:
- 1.12.x
- 1.13.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
@@ -61,9 +61,9 @@ matrix:
skip_cleanup: true
- name: docker
if: type != pull_request AND (branch = master OR tag IS present)
if: type != pull_request AND (branch = master OR tag IS present) AND repo = AdguardTeam/AdGuardHome
go:
- 1.12.x
- 1.13.x
os:
- linux
services:

View File

@@ -7,6 +7,7 @@
"da": "Dansk",
"de": "Deutsch",
"nl": "Dutch",
"no": "Norsk",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -26,6 +27,9 @@
"ja": "日本語",
"zh-tw": "正體中文",
"zh-cn": "简体中文",
"sr-cs": "Srpski",
"hr": "Hrvatski",
"fa": "فارسی",
"ko": "한국어"
}
}

View File

@@ -21,12 +21,17 @@ Contents:
* Add client
* Update client
* Delete client
* API: Find clients by IP
* Enable DHCP server
* "Show DHCP status" command
* "Check DHCP" command
* "Enable DHCP" command
* Static IP check/set
* Add a static lease
* API: Reset DHCP configuration
* DNS general settings
* API: Get DNS general settings
* API: Set DNS general settings
* DNS access settings
* List access settings
* Set access settings
@@ -59,7 +64,7 @@ Contents:
## Relations between subsystems
![](agh-arch.png)
![](doc/agh-arch.png)
@@ -542,6 +547,20 @@ Response:
200 OK
### API: Reset DHCP configuration
Clear all DHCP leases and configuration settings.
DHCP server will be stopped if it's currently running.
Request:
POST /control/dhcp/reset
Response:
200 OK
## TLS
@@ -618,8 +637,6 @@ Notes:
* `name`, `ip` and `mac` values are unique.
* `ip` & `mac` values can't be set both at the same time.
* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.
* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.
@@ -643,8 +660,7 @@ Response:
clients: [
{
name: "client1"
ip: "..."
mac: "..."
ids: ["...", ...] // IP, CIDR or MAC
use_global_settings: true
filtering_enabled: false
parental_enabled: false
@@ -656,6 +672,7 @@ Response:
key: "value"
...
}
upstreams: ["upstream1", ...]
}
]
auto_clients: [
@@ -682,8 +699,7 @@ Request:
{
name: "client1"
ip: "..."
mac: "..."
ids: ["...", ...] // IP, CIDR or MAC
use_global_settings: true
filtering_enabled: false
parental_enabled: false
@@ -691,6 +707,7 @@ Request:
safesearch_enabled: false
use_global_blocked_services: true
blocked_services: [ "name1", ... ]
upstreams: ["upstream1", ...]
}
Response:
@@ -712,8 +729,7 @@ Request:
name: "client1"
data: {
name: "client1"
ip: "..."
mac: "..."
ids: ["...", ...] // IP, CIDR or MAC
use_global_settings: true
filtering_enabled: false
parental_enabled: false
@@ -721,6 +737,7 @@ Request:
safesearch_enabled: false
use_global_blocked_services: true
blocked_services: [ "name1", ... ]
upstreams: ["upstream1", ...]
}
}
@@ -752,6 +769,85 @@ Error response (Client not found):
400
### API: Find clients by IP
This method returns the list of clients (manual and auto-clients) matching the IP list.
For auto-clients only `name`, `ids` and `whois_info` fields are set. Other fields are empty.
Request:
GET /control/clients/find?ip0=...&ip1=...&ip2=...
Response:
200 OK
[
{
"1.2.3.4": {
name: "client1"
ids: ["...", ...] // IP, CIDR or MAC
use_global_settings: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safesearch_enabled: false
use_global_blocked_services: true
blocked_services: [ "name1", ... ]
whois_info: {
key: "value"
...
}
}
}
...
]
## DNS general settings
### API: Get DNS general settings
Request:
GET /control/dns_info
Response:
200 OK
{
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
}
### API: Set DNS general settings
Request:
POST /control/dns_config
{
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
}
Response:
200 OK
`blocking_ipv4` and `blocking_ipv6` values are active when `blocking_mode` is set to `custom_ip`.
## DNS access settings
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
@@ -1012,17 +1108,21 @@ Response:
When a new DNS request is received and processed, we store information about this event in "query log". It is a file on disk in JSON format:
{
"Question":"...","
Answer":"...",
"IP":"127.0.0.1", // client IP
"T":"...", // response time
"QH":"...", // target host name without the last dot
"QT":"...", // question type
"QC":"...", // question class
"Answer":"...",
"OrigAnswer":"...",
"Result":{
"IsFiltered":true,
"Reason":3,
"Rule":"...",
"FilterID":1
"FilterID":1,
},
"Time":"...",
"Elapsed":12345,
"IP":"127.0.0.1"
"Upstream":"...",
}
@@ -1052,7 +1152,7 @@ Request:
&filter_question_type=A | AAAA
&filter_response_status= | filtered
If `older_than` value is set, server returns the next chunk of entries that are older than this time stamp. This setting is used for paging. UI sets the empty value on the first request and gets the latest log entries. To get the older entries, UI sets this value to the timestamp of the last (the oldest) entry from the previous response from Server.
`older_than` setting is used for paging. UI uses an empty value for `older_than` on the first request and gets the latest log entries. To get the older entries, UI sets `older_than` to the `oldest` value from the server's response.
If "filter" settings are set, server returns only entries that match the specified request.
@@ -1060,7 +1160,9 @@ For `filter.domain` and `filter.client` the server matches substrings by default
Response:
[
{
"oldest":"2006-01-02T15:04:05.999999999Z07:00"
"data":[
{
"answer":[
{
@@ -1070,6 +1172,13 @@ Response:
}
...
],
"original_answer":[ // Answer from upstream server (optional)
{
"type":"AAAA",
"value":"::"
}
...
],
"client":"127.0.0.1",
"elapsedMs":"0.098403",
"filterId":1,
@@ -1080,11 +1189,13 @@ Response:
},
"reason":"FilteredBlackList",
"rule":"||doubleclick.net^",
"service_name": "...", // set if reason=FilteredBlockedService
"status":"NOERROR",
"time":"2006-01-02T15:04:05.999999999Z07:00"
}
...
]
}
The most recent entries are at the top of list.
@@ -1123,6 +1234,26 @@ Response:
## Filtering
![](doc/agh-filtering.png)
This is how DNS requests and responses are filtered by AGH:
* 'dnsproxy' module receives DNS request from client and passes control to AGH
* AGH applies filtering logic to the host name in DNS Question:
* process Rewrite rules
* match host name against filtering lists
* match host name against blocked services rules
* process SafeSearch rules
* request SafeBrowsing & ParentalControl services and process their response
* If the handlers above create a successful result that can be immediately sent to a client, it's passed back to 'dnsproxy' module
* Otherwise, AGH passes the DNS request to an upstream server via 'dnsproxy' module
* After 'dnsproxy' module has received a response from an upstream server, it passes control back to AGH
* If the filtering logic for DNS request returned a 'whitelist' flag, AGH passes the response to a client
* Otherwise, AGH applies filtering logic to each DNS record in response:
* For CNAME records, the target name is matched against filtering lists (ignoring 'whitelist' rules)
* For A and AAAA records, the IP address is matched against filtering lists (ignoring 'whitelist' rules)
### Filters update mechanism
Filters can be updated either manually by request from UI or automatically.

View File

@@ -14,8 +14,8 @@
<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 href="https://travis-ci.com/AdguardTeam/AdGuardHome">
<img src="https://travis-ci.com/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" />
@@ -127,7 +127,7 @@ It depends.
You will need:
* [go](https://golang.org/dl/) v1.12 or later.
* [go](https://golang.org/dl/) v1.13 or later.
* [node.js](https://nodejs.org/en/download/) v10 or later.
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:

6
ci.sh
View File

@@ -16,14 +16,14 @@ golangci-lint --version
# Run linter
golangci-lint run
# Run tests
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
# Make
make clean
make build/static/index.html
make
# Run tests
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
# if [[ -z "$(git status --porcelain)" ]]; then
# # Working directory clean
# echo "Git status is clean"

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

@@ -5214,8 +5214,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -5236,14 +5235,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5258,20 +5255,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5388,8 +5382,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -5401,7 +5394,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5416,7 +5408,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5424,14 +5415,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5450,7 +5439,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5531,8 +5519,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -5544,7 +5531,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -5630,8 +5616,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5667,7 +5652,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5687,7 +5671,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5731,14 +5714,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta name="google" content="notranslate">
<meta http-equiv="x-dns-prefetch-control" content="off">
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
<title>AdGuard Home</title>
</head>

View File

@@ -11,9 +11,7 @@
"dhcp_found": "Вашата мрежа вече има активен DHCP сървър. Не е безопасно ползването на втори!",
"dhcp_leases": "DHCP раздадени адреси",
"dhcp_leases_not_found": "Няма намерени активни DHCP адреси",
"dhcp_config_saved": "Запиши конфигурацията на DHCP сървъра",
"form_error_required": "Задължително поле",
"form_error_ip_format": "Невалиден IPv4 адрес",
"form_error_positive": "Проверете дали е положително число",
"dhcp_form_gateway_input": "IP шлюз",
"dhcp_form_subnet_input": "Мрежова маска",
@@ -71,11 +69,9 @@
"use_adguard_parental": "Включи AdGuard Родителски Надзор",
"use_adguard_parental_hint": "Модул XXX в AdGuard Home ще провери дали страницата има материали за възвъстни. Използва се същия API за анонимност като при модула за Сигурност.",
"enforce_safe_search": "Включи Безопасно Търсене",
"enforce_save_search_hint": "AdGuard Home прилага Безопасно Търсене в следните търсачки и сайтове: Google, Youtube, Bing, и Yandex.",
"no_servers_specified": "Няма избрани услуги",
"general_settings": "Общи настройки",
"upstream_dns": "Главен DNS сървър",
"upstream_dns_hint": "Ако оставите празно, AdGuard Home ще използва <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> за главен. Използвай tls:// представка за DNS използващи TLS връзка.",
"test_upstream_btn": "Тествай главния DNS",
"apply_btn": "Приложи",
"disabled_filtering_toast": "Забрани филтрирането",

View File

@@ -19,7 +19,9 @@
"dhcp_leases_not_found": "Nebyly nalezeny žádné pronájmy DHCP",
"dhcp_config_saved": "Konfigurace DHCP serveru byla uložena",
"form_error_required": "Povinné pole",
"form_error_ip_format": "Neplatný formát IPv4",
"form_error_ip4_format": "Neplatný formát IPv4",
"form_error_ip6_format": "Neplatný formát IPv6",
"form_error_ip_format": "Neplatný formát IP",
"form_error_mac_format": "Neplatný formát MAC",
"form_error_positive": "Musí být větší než 0",
"dhcp_form_gateway_input": "IP brána",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nový statický pronájem",
"dhcp_static_leases_not_found": "Nebyly nalezeny žádné statické pronájmy DHCP",
"dhcp_add_static_lease": "Přidat statický pronájem",
"dhcp_reset": "Opravdu chcete resetovat konfiguraci DHCP?",
"delete_confirm": "Opravdu chcete odstranit \"{{key}}\"?",
"form_enter_hostname": "Zadejte název hostitele",
"error_details": "Podrobnosti chyby",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Použít službu AdGuard Rodičovská kontrola",
"use_adguard_parental_hint": "AdGuard Home zkontroluje, zda doména obsahuje materiály pro dospělé. Používá stejné API přátelské k ochraně osobních údajů jako služba Bezpečnost prohlížení.",
"enforce_safe_search": "Vynutit bezpečné vyhledávání",
"enforce_save_search_hint": "AdGuard Home může vynutit bezpečné vyhledávání v následujících vyhledávačích: Google, YouTube, Bing DuckDuckGo a Yandex.",
"enforce_save_search_hint": "AdGuard Home může vynutit bezpečné vyhledávání v následujících vyhledávačích: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Nebyly specifikovány žádné servery",
"general_settings": "Obecná nastavení",
"dns_settings": "Nastavení DNS",
"encryption_settings": "Nastavení šifrování",
"dhcp_settings": "Nastavení DHCP",
"upstream_dns": "Upstream DNS servery",
"upstream_dns_hint": "Pokud toto pole ponecháte prázdné, AdGuard Home použije <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> jako upstream.",
"upstream_dns_hint": "Pokud toto pole ponecháte prázdné, AdGuard Home použije <a href='https://www.quad9.net/' target='_blank'>Quad9</a> jako upstream.",
"test_upstream_btn": "Test upstreamů",
"upstreams": "Upstreamy",
"apply_btn": "Použít",
"disabled_filtering_toast": "Vypnuté filtrování",
"enabled_filtering_toast": "Zapnuté filtrování",
@@ -279,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} je nyní k dispozici! <0>Klikněte zde<0> pro více informací.",
"setup_guide": "Průvodce nastavením",
"dns_addresses": "Adresy DNS",
"dns_start": "Spustí se server DNS",
"dns_status_error": "Chyba při získávání stavu DNS serveru",
"down": "Dolů",
"fix": "Opravit",
"dns_providers": "Zde je <0>seznam známých poskytovatelů DNS</0>, z nichž si můžete vybrat.",
@@ -297,9 +303,11 @@
"client_edit": "Upravit klienta",
"client_identifier": "Identifikátor",
"ip_address": "IP adresa",
"client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy nebo MAC adresy. Upozorňujeme, že použití MAC jako identifikátoru je možné pouze v případě, že je AdGuard Home také <0>DHCP server</0>",
"client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy CIDR nebo MAC adresy. Upozorňujeme, že použití MAC jako identifikátoru je možné pouze v případě, že je AdGuard Home také <0>DHCP server</0>",
"form_enter_ip": "Zadejte IP",
"form_enter_mac": "Zadejte MAC",
"form_enter_id": "Zadejte identifikátor",
"form_add_id": "Přidat identifikátor",
"form_client_name": "Zadejte název klienta",
"client_global_settings": "Použít globální nastavení",
"client_deleted": "Klient \"{{key}}\" byl úspěšně odstraněn",
@@ -400,5 +408,6 @@
"netname": "Název sítě",
"descr": "Popis",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Další informace</0> o vytváření vlastních seznamů zakázaných hostitelů."
"filtering_rules_learn_more": "<0>Další informace</0> o vytváření vlastních seznamů zakázaných hostitelů.",
"blocked_by_response": "Zakázáno s odpovědí CNAME nebo IP"
}

View File

@@ -17,9 +17,11 @@
"dhcp_leases": "DHCP-leases",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "Ingen DHCP-leases fundet",
"dhcp_config_saved": "Gemt DHCP-server konfiguration",
"dhcp_config_saved": "DHCP-konfiguration gemt",
"form_error_required": "Obligatorisk felt",
"form_error_ip_format": "Ugyldigt IPv4-format",
"form_error_ip4_format": "Ugyldigt IPv4-format",
"form_error_ip6_format": "Ugyldigt IPv6-format",
"form_error_ip_format": "Ugyldigt IP-format",
"form_error_mac_format": "Ugyldigt MAC-format",
"form_error_positive": "Skal være større end 0",
"dhcp_form_gateway_input": "Gateway IP",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Ny static lease",
"dhcp_static_leases_not_found": "Ingen DHCP static leases fundet",
"dhcp_add_static_lease": "Tilføj static lease",
"dhcp_reset": "Er du sikker på, at du vil nulstille DHCP-konfigurationen?",
"delete_confirm": "Er du sikker på, at du vil slette \"{{key}}\"?",
"form_enter_hostname": "Indtast værtsnavn",
"error_details": "Fejloplysninger",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Brug AdGuards forældrekontrol",
"use_adguard_parental_hint": "AdGuard Home vil kontrollere, om domænet indeholder voksenindhold. Den bruger den samme privatlivsvenlige API som browsing sikkerhedstjenesten.",
"enforce_safe_search": "Håndhæv sikker søgning",
"enforce_save_search_hint": "AdGuard Home kan håndhæve sikker søgning i følgende søgemaskiner: Google, Youtube, Bing, DuckDuckGo og Yandex.",
"enforce_save_search_hint": "AdGuard Home kan håndhæve sikker søgning i følgende søgemaskiner: Google, Youtube, Bing, DuckDuckGo, Yandex og Pixabay.",
"no_servers_specified": "Ingen servere specificeret",
"general_settings": "Generelle indstillinger",
"dns_settings": "DNS-indstillinger",
"encryption_settings": "Krypteringsindstillinger",
"dhcp_settings": "DHCP-indstillinger",
"upstream_dns": "Upstream DNS-servere",
"upstream_dns_hint": "Hvis du lader dette felt være tomt, vil AdGuard Home bruge <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> til upstream.",
"upstream_dns_hint": "Hvis du lader dette felt være tomt, vil AdGuard Home bruge <a href='https://www.quad9.net/' target='_blank'>Quad9</a> som en upstream.",
"test_upstream_btn": "Test upstreams",
"upstreams": "Upstreams",
"apply_btn": "Anvend",
"disabled_filtering_toast": "Filtrering deaktiveret",
"enabled_filtering_toast": "Filtrering aktiveret",
@@ -279,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} er nu tilgængelig! <0>Kik her</0> for mere info.",
"setup_guide": "Installationsvejledning",
"dns_addresses": "DNS-adresser",
"dns_start": "DNS-server starter",
"dns_status_error": "Fejl ved at få DNS-serverstatus",
"down": "Ned",
"fix": "Reparer",
"dns_providers": "Her er en <0>liste over kendte DNS-udbydere</ 0> at vælge imellem.",
@@ -297,9 +303,11 @@
"client_edit": "Rediger Klient",
"client_identifier": "Identifikator",
"ip_address": "IP-adresse",
"client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen eller MAC-adressen. Bemærk venligst, at det kun er muligt at bruge MAC som identifikator, hvis AdGuard Home også er en <0>DHCP-server</0>",
"client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen. Bemærk venligst, at det kun er muligt at bruge MAC som identifikator, hvis AdGuard Home også er en <0>DHCP-server</0>",
"form_enter_ip": "Indtast IP",
"form_enter_mac": "Indtast MAC",
"form_enter_id": "Indtast identifikator",
"form_add_id": "Tilføj identifikator",
"form_client_name": "Indtast klientnavn",
"client_global_settings": "Brug globale indstillinger",
"client_deleted": "Klient \"{{key}}\" succesfuldt slettet",
@@ -399,5 +407,7 @@
"orgname": "Organisationens navn",
"netname": "Netværksnavn",
"descr": "Beskrivelse",
"whois": "Whois"
"whois": "Whois",
"filtering_rules_learn_more": "<0>Lær mere</0> om at oprette dine egne værtsblokeringslister.",
"blocked_by_response": "Blokeret af CNAME eller IP som svar"
}

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP-Leasingverträge",
"dhcp_static_leases": "DHCP statische Leases",
"dhcp_leases_not_found": "Keine DHCP-Leasingverträge gefunden\n",
"dhcp_config_saved": "Gespeicherte DHCP-Server-Konfiguration",
"form_error_required": "Pflichtfeld",
"form_error_ip_format": "Ungültiges IPv4-Format",
"form_error_mac_format": "Ungültiges MAC-Format",
"form_error_positive": "Muss größer als 0 sein.",
"dhcp_form_gateway_input": "Gateway-IP",
@@ -96,14 +94,12 @@
"use_adguard_parental": "AdGuard Webservice für Kindersicherung verwenden",
"use_adguard_parental_hint": "AdGuard Home wird überprüfen, ob die Domain Inhalte hat, die nur für Erwachsene geeignet sind. Zum Schutz Ihrer Privatsphäre wird die gleiche API wie für den Webservice für Internet-Sicherheit verwendet.",
"enforce_safe_search": "SafeSearch erzwingen",
"enforce_save_search_hint": "AdGuard kann SafeSearch für folgende Suchmaschinen erzwingen: Google, Youtube, Bing und Yandex.",
"no_servers_specified": "Keine Server festgelegt",
"general_settings": "Allgemeine Einstellungen",
"dns_settings": "DNS-Einstellungen",
"encryption_settings": "Verschlüsselungseinstellungen",
"dhcp_settings": "DHCP-Einstellungen",
"upstream_dns": "Upstream-DNS-Server",
"upstream_dns_hint": "Wenn Sie dieses Feld leer lassen wird AdGuard Home <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> als Upstream verwenden. Verwenden Sie das Präfix tls:// für DNS über TLS-Server.",
"test_upstream_btn": "Upstreams testen",
"apply_btn": "Anwenden",
"disabled_filtering_toast": "Filtern deaktiviert",
@@ -297,7 +293,6 @@
"client_edit": "Client bearbeiten",
"client_identifier": "Bezeichner",
"ip_address": "IP-Adresse",
"client_identifier_desc": "Clients können durch die IP-Adresse oder MAC-Adresse identifiziert werden. Bitte beachten Sie, dass die Verwendung der MAC-Adresse als Identifikator nur möglich ist, wenn AdGuard Home gleichzeitig auch ein <0>DHCP-Server</0> ist.",
"form_enter_ip": "IP-Adresse eingeben",
"form_enter_mac": "MAC-Adresse eingeben",
"form_client_name": "Clientnamen eingeben",

View File

@@ -17,11 +17,14 @@
"dhcp_leases": "DHCP leases",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "No DHCP leases found",
"dhcp_config_saved": "Saved DHCP server config",
"dhcp_config_saved": "DHCP config successfully saved",
"form_error_required": "Required field",
"form_error_ip_format": "Invalid IPv4 format",
"form_error_ip4_format": "Invalid IPv4 format",
"form_error_ip6_format": "Invalid IPv6 format",
"form_error_ip_format": "Invalid IP format",
"form_error_mac_format": "Invalid MAC format",
"form_error_positive": "Must be greater than 0",
"form_error_negative": "Must be equal to 0 or greater",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Range of IP addresses",
@@ -43,6 +46,7 @@
"dhcp_new_static_lease": "New static lease",
"dhcp_static_leases_not_found": "No DHCP static leases found",
"dhcp_add_static_lease": "Add static lease",
"dhcp_reset": "Are you sure you want to reset DHCP config?",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Enter hostname",
"error_details": "Error details",
@@ -96,15 +100,16 @@
"use_adguard_parental": "Use AdGuard parental control web service",
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
"enforce_safe_search": "Enforce safe search",
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo and Yandex.",
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "No servers specified",
"general_settings": "General settings",
"dns_settings": "DNS settings",
"encryption_settings": "Encryption settings",
"dhcp_settings": "DHCP settings",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> as an upstream.",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https://www.quad9.net/' target='_blank'>Quad9</a> as an upstream.",
"test_upstream_btn": "Test upstreams",
"upstreams": "Upstreams",
"apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering",
"enabled_filtering_toast": "Enabled filtering",
@@ -183,6 +188,22 @@
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
"query_log_strict_search": "Use double quotes for strict search",
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost",
"dns_config": "DNS server configuration",
"blocking_mode": "Blocking mode",
"nxdomain": "NXDOMAIN",
"null_ip": "Null IP",
"custom_ip": "Custom IP",
"blocking_ipv4": "Blocking IPv4",
"blocking_ipv6": "Blocking IPv6",
"form_enter_rate_limit": "Enter rate limit",
"rate_limit": "Rate limit",
"edns_enable": "Enable EDNS Client Subnet",
"edns_cs_desc": "If enabled, AdGuard Home will be sending clients' subnets to the DNS servers.",
"rate_limit_desc": "The number of requests per second that a single client is allowed to make (0: unlimited)",
"blocking_ipv4_desc": "IP address to be returned for a blocked A request",
"blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request",
"blocking_mode_desc": "<0>NXDOMAIN Respond with NXDOMAIN code;</0> <0>Null IP Respond with zero IP address (0.0.0.0 for A; :: for AAAA);</0> <0>Custom IP - Respond with a manually set IP address.</0>",
"upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.",
"source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category",
@@ -279,6 +300,8 @@
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info.",
"setup_guide": "Setup guide",
"dns_addresses": "DNS addresses",
"dns_start": "DNS server is starting up",
"dns_status_error": "Error of getting DNS server status",
"down": "Down",
"fix": "Fix",
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
@@ -297,9 +320,11 @@
"client_edit": "Edit Client",
"client_identifier": "Identifier",
"ip_address": "IP address",
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
"form_enter_ip": "Enter IP",
"form_enter_mac": "Enter MAC",
"form_enter_id": "Enter identifier",
"form_add_id": "Add identifier",
"form_client_name": "Enter client name",
"client_global_settings": "Use global settings",
"client_deleted": "Client \"{{key}}\" successfully deleted",
@@ -400,5 +425,7 @@
"netname": "Network name",
"descr": "Description",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Learn more</0> about creating your own hosts blocklists."
"filtering_rules_learn_more": "<0>Learn more</0> about creating your own hosts blocklists.",
"blocked_by_response": "Blocked by CNAME or IP in response",
"try_again": "Try again"
}

View File

@@ -17,9 +17,11 @@
"dhcp_leases": "Asignaciones DHCP",
"dhcp_static_leases": "Asignaciones DHCP estáticas",
"dhcp_leases_not_found": "No se han encontrado asignaciones DHCP",
"dhcp_config_saved": "Configuración del servidor DHCP guardada",
"dhcp_config_saved": "Configuración DHCP guardado correctamente",
"form_error_required": "Campo obligatorio",
"form_error_ip_format": "Formato IPv4 no válido",
"form_error_ip4_format": "Formato IPv4 no válido",
"form_error_ip6_format": "Formato IPv6 no válido",
"form_error_ip_format": "Formato IP no válido",
"form_error_mac_format": "Formato MAC no válido",
"form_error_positive": "Debe ser mayor que 0",
"dhcp_form_gateway_input": "IP de puerta de enlace",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nueva asignación estática",
"dhcp_static_leases_not_found": "No se han encontrado asignaciones DHCP estáticas",
"dhcp_add_static_lease": "Añadir asignación estática",
"dhcp_reset": "¿Está seguro de que desea restablecer la configuración DHCP?",
"delete_confirm": "¿Está seguro de que desea eliminar \"{{key}}\"?",
"form_enter_hostname": "Ingrese el nombre del host",
"error_details": "Detalles del error",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Usar el control parental de AdGuard",
"use_adguard_parental_hint": "AdGuard Home comprobará si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad del servicio web de seguridad de navegación.",
"enforce_safe_search": "Forzar búsqueda segura",
"enforce_save_search_hint": "AdGuard Home puede forzar la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo y Yandex.",
"enforce_save_search_hint": "AdGuard Home puede forzar la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Yandex y Pixabay.",
"no_servers_specified": "No hay servidores especificados",
"general_settings": "Configuración general",
"dns_settings": "Configuración del DNS",
"encryption_settings": "Configuración de cifrado",
"dhcp_settings": "Configuración DHCP",
"upstream_dns": "Servidores DNS de subida",
"upstream_dns_hint": "Si mantiene este campo vacío, AdGuard Home utilizará <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> como DNS de subida.",
"upstream_dns_hint": "Si mantiene este campo vacío, AdGuard Home utilizará <a href='https://www.quad9.net/' target='_blank'>Quad9</a> como DNS de subida.",
"test_upstream_btn": "Probar DNS de subida",
"upstreams": "DNS de subida",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtrado deshabilitado",
"enabled_filtering_toast": "Filtrado habilitado",
@@ -183,6 +187,12 @@
"query_log_disabled": "El registro de consultas está deshabilitado y se puede configurar en la <0>configuración</0>",
"query_log_strict_search": "Usar comillas dobles para una búsqueda estricta",
"query_log_retention_confirm": "¿Está seguro de que desea cambiar la retención del registro de consultas? Si disminuye el valor del intervalo, se perderán algunos datos",
"blocking_mode": "Modo de bloqueo",
"nxdomain": "NXDOMAIN",
"custom_ip": "IP personalizada",
"edns_cs_desc": "Si está habilitado, AdGuard Home enviará las subredes de los clientes a los servidores DNS.",
"rate_limit_desc": "Número de peticiones por segundo que un solo cliente puede hacer (0: ilimitado)",
"upstream_dns_client_desc": "Si mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración del DNS</0>.",
"source_label": "Fuente",
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
"category_label": "Categoría",
@@ -279,6 +289,8 @@
"update_announcement": "¡AdGuard Home {{version}} ya está disponible! <0>Haga clic aquí</0> para más información.",
"setup_guide": "Guía de configuración",
"dns_addresses": "Direcciones DNS",
"dns_start": "El servidor DNS está iniciando",
"dns_status_error": "Error al obtener el estado del servidor DNS",
"down": "Abajo",
"fix": "Corregir",
"dns_providers": "Aquí hay una <0>lista de proveedores DNS</0> conocidos para elegir.",
@@ -297,9 +309,11 @@
"client_edit": "Editar cliente",
"client_identifier": "Identificador",
"ip_address": "Dirección IP",
"client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP o MAC. Tenga en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home también es un <0>servidor DHCP</0>.",
"client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC y CIDR. Tenga en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home también es un <0>servidor DHCP</0>",
"form_enter_ip": "Ingresar IP",
"form_enter_mac": "Ingresar MAC",
"form_enter_id": "Ingrese el identificador",
"form_add_id": "Añadir identificador",
"form_client_name": "Ingrese el nombre del cliente",
"client_global_settings": "Usar configuración global",
"client_deleted": "Cliente \"{{key}}\" eliminado correctamente",
@@ -400,5 +414,6 @@
"netname": "Nombre de la red",
"descr": "Descripción",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Más información</0> sobre cómo crear tus propias listas para bloqueo de hosts."
"filtering_rules_learn_more": "<0>Más información</0> sobre cómo crear tus propias listas para bloqueo de hosts.",
"blocked_by_response": "Bloqueado por CNAME o IP en respuesta"
}

View File

@@ -0,0 +1,366 @@
{
"client_settings": "تنظیمات کلاینت",
"example_upstream_reserved": "میتوانید جریان ارسالی DNS <0> را برای یک دامنه مشخص تعیین کنید </0>",
"upstream_parallel": "استفاده از جستار موازی برای سرعت دادن به تفکیک با جستار همزمان همه جریان های ارسالی",
"bootstrap_dns": "خودراه انداز سرورهای DNS",
"bootstrap_dns_desc": "خودراه انداز سرورهای DNS برای تفکیک آدرس آی پی تفکیک کننده های DoH/DoT که شما بعنوان جریان ارسالی تعیین کردید استفاده میشود.",
"check_dhcp_servers": "بررسی برای سرورهای DHCP",
"save_config": "ذخیره پیکربندی",
"enabled_dhcp": "سرور DHCP فعال شده است",
"disabled_dhcp": "سرور DHCP غیرفعال شده است",
"dhcp_title": "سرور DHCP",
"dhcp_description": "اگر روتر شما تنظیمات DHCP ارائه نمی کند،میتوانید از سرور DHCP تو-کار خود AdGuard استفاده کنید.",
"dhcp_enable": "فعالسازی سرور DHCP",
"dhcp_disable": "غیرفعالسازی سرور DHCP",
"dhcp_not_found": "سرورهای فعال DHCP در شبکه یافت نشد.فعالسازی سرور DHCP تو-کار اَمن است.",
"dhcp_found": "تعدادی سرور DHCP فعال در شبکه یافت شد.فعالسازی سرور DHCP توکار اَمن نیست.",
"dhcp_leases": "اجاره DHCP",
"dhcp_static_leases": "اجاره DHCP ایستا",
"dhcp_leases_not_found": "اجاره DHCP یافت نشد",
"form_error_required": "فیلد مورد نیاز",
"form_error_mac_format": "فرمت مَک نامعتبر است",
"form_error_positive": "باید بزرگتر از 0 باشد",
"dhcp_form_gateway_input": "آی پی دروازه",
"dhcp_form_subnet_input": "ماسک زیر شبکه",
"dhcp_form_range_title": "دامنه آدرس های آی پی",
"dhcp_form_range_start": "آغاز دامنه",
"dhcp_form_range_end": "انتهای دامنه",
"dhcp_form_lease_title": "زمان اجاره DHCP (در ثانیه)",
"dhcp_form_lease_input": "مدت اجاره",
"dhcp_interface_select": "رابط DHCP را انتخاب کنید",
"dhcp_hardware_address": "آدرس سخت افزار",
"dhcp_ip_addresses": "آدرس آی پی",
"dhcp_table_hostname": "نام میزبان",
"dhcp_table_expires": "انقضاء",
"dhcp_warning": "اگر میخواهید DHCP سرور توکار را فعال کنید،مطمئن شوید DHCP سرور دیگری فعال نباشد.در غیر اینصورت،آن دسترسی به اینترنت را برای دستگاه های وصل شده قطع می کند!",
"dhcp_error": "ما نمیتوانیم تشخیص دهیم آیا یک سرور DHCP دیگر در شبکه موجود است یا نه.",
"dhcp_static_ip_error": "به منظور استفاده از سرور DHCP یک آدرس آی پی ثابت باید تنظیم شود.ما موفق به تشخیص اینکه آیا رابط این شبکه برای استفاده از آی پی ثابت تنظیم شده است یا نه موفق نشدیم.لطفا آدرس آی پی ثابت را دستی تنظیم کنید.",
"dhcp_dynamic_ip_found": "سیستم شما آدرس آی پی متغییر برای این رابط استفاده می کند <0>{{interfaceName}}</0>. به منظور استفاده از سرور DHCP آدرس ثابت باید تعیین شود. آدرس آی پی فعلی شما هست <0>{{ipAddress}}</0>. اگر شما دکمه DHCP را فشار دهید ما این آدرس آی پی را بعنوان ثابت تنظیم می کنیم.",
"dhcp_lease_added": "اجاره ایستا \"{{key}}\" با موفقیت اضافه شد",
"dhcp_lease_deleted": "اجاره ایستا \"{{key}}\" با موفقیت حذف شد",
"dhcp_new_static_lease": "اجاره ایستا جدید",
"dhcp_static_leases_not_found": "هیچ اجاره DHCP ایستا یافت نشد",
"dhcp_add_static_lease": "افزودن اجاره ایستا",
"delete_confirm": "آیا میخواهید \"{{key}}\" را حذف کنید؟",
"form_enter_hostname": "نام میزبان را وارد کنید",
"error_details": "جزئیات خطا",
"back": "قبلی",
"dashboard": "داشبورد",
"settings": "تنظيمات",
"filters": "فيلترها",
"query_log": "جستار وقایع",
"faq": "پرسش و پاسخ",
"version": "نسخه",
"address": "آدرس",
"on": "روشن",
"off": "خاموش",
"copyright": "حق مالکیت",
"homepage": "صفحه خانگي",
"report_an_issue": "گزارش یک مشکل",
"privacy_policy": "سیاست حریم خصوصی",
"enable_protection": "فعالسازي حفاظت",
"enabled_protection": "حفاظت فعال شده",
"disable_protection": "غيرفعالسازي حفاظت",
"disabled_protection": "حفاظت غير فعال شده",
"refresh_statics": "تازه سازی آمار",
"dns_query": "جستار DNS",
"blocked_by": "مسدود شده با",
"stats_malware_phishing": "بدافزار/فیشینگ مسدود شده است",
"stats_adult": "وبسایت غیراخلاقی مسدود شده است",
"stats_query_domain": "دامنه جستار بالا",
"for_last_24_hours": "برای 24 ساعت گذشته",
"for_last_days": "برای {{value}} روز آخر",
"for_last_days_plural": "برای {{count}} روز گذشته",
"no_domains_found": "دامنه یافت نشد",
"requests_count": "تعداد درخواست ها",
"top_blocked_domains": "دامنه های بیشتر مسدود شده",
"top_clients": "بالاترین کلاینت ها",
"no_clients_found": "کلاینتی یافت نشد",
"general_statistics": "آمار عمومی",
"number_of_dns_query_days": "تعداد جستار DNS پردازش شده در {{value}} روز آخر",
"number_of_dns_query_days_plural": "تعداد جستار DNS پردازش شده در {{count}} روز گذشته",
"number_of_dns_query_24_hours": "تعداد جستار DNS پردازش شده در 24 ساعت گذشته",
"number_of_dns_query_blocked_24_hours": "تعداد درخواست DNS مسدود شده با فیلترهای مسدودساز تبلیغ و لیست سیاه میزبان",
"number_of_dns_query_blocked_24_hours_by_sec": "تعداد درخواست DNS مسدود شده با مدل امنیت وب گردی AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "تعداد وبسایت های غیر اخلاقی مسدود شده",
"enforced_save_search": "جستجوی اَمن اجبار شده",
"number_of_dns_query_to_safe_search": "تعداد درخواست های DNS برای موتور جستجو که جستجوی اَمن اجبار شده",
"average_processing_time": "میانگین زمان پردازش",
"average_processing_time_hint": "زمان میانگین بر هزارم ثانیه در پردازش درخواست DNS",
"block_domain_use_filters_and_hosts": "مسدودسازی دامنه ها توسط فیلترها و فایل های میزبان",
"filters_block_toggle_hint": "میتوانید دستورات مسدودسازی را در تنظیمات <a href='#filters'>فیلترها</a> راه اندازی کنید.",
"use_adguard_browsing_sec": "استفاده از سرویس وب امنیت وب گردی AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home بررسی می کند اگر دامنه در سرویس وب امنیت وب گردی در لیست سیاه است.آن از اِی پی آی دارای حریم خصوصی برای بررسی استفاده می کند:فقط پیشوند کوتاه نام دامنه هش SHA256 به سرور ارسال خواهد شد.",
"use_adguard_parental": "از سرویس وب نظارت والدین AdGuard استفاده کن",
"use_adguard_parental_hint": "AdGuard Home بررسی می کند اگر دامنه حاوی موارد غیر اخلاقی است.آن از همان اِی پی آی دارای حریم خصوصی سرویس وب امنیت وب گردی استفاده می کند.",
"enforce_safe_search": "اجبار جستجوی اَمن",
"no_servers_specified": "سروری تعیین نشده است",
"general_settings": "تنظیمات عمومی",
"dns_settings": "تنظیمات DNS",
"encryption_settings": "تنظیمات رمزگُذاری",
"dhcp_settings": "تنظیمات DHCP",
"upstream_dns": "سرورهای DNS جریان ارسالی",
"test_upstream_btn": "تست جریان ارسالی",
"apply_btn": "اِعمال",
"disabled_filtering_toast": "فیلترینگ غیرفعال شده است",
"enabled_filtering_toast": "فیلترینگ فعال شده است",
"disabled_safe_browsing_toast": "وب گردی اَمن غیر فعال شده است",
"enabled_safe_browsing_toast": "وب گردی اَمن فعال شده است",
"disabled_parental_toast": "نظارت والدین غیرفعال شده است",
"enabled_parental_toast": "نظارت والدین فعال شده است",
"disabled_safe_search_toast": "جستجوی اَمن غیرفعال شده",
"enabled_save_search_toast": "جستجوی اَمن فعال شده",
"enabled_table_header": "فعال شده",
"name_table_header": "نام",
"filter_url_table_header": "فیلتر آدرس",
"rules_count_table_header": "تعداد دستور",
"last_time_updated_table_header": "زمان آخرین بروزرسانی",
"actions_table_header": "اقدامات",
"edit_table_action": "ويرايش",
"delete_table_action": "حذف",
"filters_and_hosts": "فیلترها و میزبان های لیست سیاه",
"filters_and_hosts_hint": "AdGuard Home دستورات پایه مسدودساز تبلیغ و نحو فایل های میزبان را درک می کند.",
"no_filters_added": "فیلتری اضافه نشده است",
"add_filter_btn": "افزودن فیلتر",
"cancel_btn": "لغو",
"enter_name_hint": "نام را وارد کنید",
"enter_url_hint": "آدرس را وارد کنید...",
"check_updates_btn": "بررسی بروز رسانی",
"new_filter_btn": "اشتراک فیلتر جدید",
"enter_valid_filter_url": "آدرس معتبر برای اشتراک در فیلتر یا فایل میزبان وارد کنید.",
"custom_filter_rules": "دستورات فیلترینگ دستی",
"custom_filter_rules_hint": "یک دستور در خط وارد کنید.میتوانید از دستورات مسدودساز تبلیغ یا نحو فایل های میزبان استفاده کنید.",
"examples_title": "مثال ها",
"example_meaning_filter_block": "مسدودسازی دسترسی به دامنه example.org و همه زیر دامنه ها آن",
"example_meaning_filter_whitelist": "بازکردن دسترسی به دامنه example.org و همه زیر دامنه ها آن",
"example_meaning_host_block": "AdGuard Home بر می گردد به آدرس 127.0.0.1 برای دامنه example.org (اما نه زیر دامنه های آن)",
"example_comment": "! در اینجا نظر قرار می گیرد",
"example_comment_meaning": "فقط یک توضیح",
"example_comment_hash": "# همچنین یک نظر",
"example_upstream_regular": "DNS عادی (بر UDP)",
"example_upstream_dot": "کُدگذاری شده <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>",
"example_upstream_doh": "کُدگذاری شده <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>",
"example_upstream_sdns": "شما میتوانید از <a href='https://dnscrypt.info/stamps/' target='_blank'>DNS Stamps</a> برای <a href='https://dnscrypt.info/' target='_blank'>DNSCrypt</a> یا <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a> resolvers استفاده کنید",
"example_upstream_tcp": "DNS عادی (بر TCP)",
"all_filters_up_to_date_toast": "همه فیلترها از قبل بروز رسانی شده است",
"updated_upstream_dns_toast": "سرورهای DNS جریان ارسالی بروز رسانی شده است",
"dns_test_ok_toast": "سرورهای DNS تعیین شده بدرستی کار می کنند",
"dns_test_not_ok_toast": "سرور \"{{key}}\": نمیتواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را بدرستی نوشته اید",
"unblock_btn": "باز کن",
"block_btn": "مسدود کن",
"time_table_header": "زمان",
"domain_name_table_header": "نام دامنه",
"type_table_header": "نوع",
"response_table_header": "پاسخ",
"client_table_header": "کلاینت",
"empty_response_status": "خالی",
"show_all_filter_type": "نمایش همه",
"show_filtered_type": "نمایش فیلتر شده",
"no_logs_found": "وقایع یافت نشد",
"refresh_btn": "تازه سازی",
"previous_btn": "قبلی",
"next_btn": "بعدی",
"loading_table_status": "بارگیری...",
"page_table_footer_text": "صفحه",
"of_table_footer_text": "از",
"rows_table_footer_text": "سطر",
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد",
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد",
"query_log_response_status": "وضعیت: {{value}}",
"query_log_filtered": "فیلتر شده با {{filter}}",
"source_label": "منبع",
"found_in_known_domain_db": "در پایگاه داده دامنه های شناخته شده پیدا شد",
"category_label": "دسته بندی",
"rule_label": "دستور",
"filter_label": "فیلتر",
"unknown_filter": "فیلتر ناشناخته {{filterId}}",
"install_welcome_title": "به AdGuard Home خوش آمدید!",
"install_welcome_desc": "AdGuard Home یک شبکه گسترده و ردیاب و مسدوساز تبلیغ با سرور DNS است.هدف آن این است که به شما اجازه کنترل کل شبکه و همه دستگاه های شما را بدهد و آن نیازی به برنامه سمت-کاربر ندارد.",
"install_settings_title": "رابط وب آدمین",
"install_settings_listen": "رابط گوش دادن",
"install_settings_port": "پورت",
"install_settings_interface_link": "رابط صفحه وب آدمین AdGuard Home شما در این آدرس قابل دسترسی خواهد بود:",
"form_error_port": "مقدار پورت معتبر وارد کنید",
"install_settings_dns": "سرور DNS",
"install_settings_dns_desc": "نیاز است شما دستگاه یا روتر خود را برای استفاده از سرور DNS روی آدرس های زیر پیکربندی کنید:",
"install_settings_all_interfaces": "همه رابط ها",
"install_auth_title": "احراز هویت",
"install_auth_username": "نام کاربر",
"install_auth_password": "رمزعبور",
"install_auth_confirm": "تأیید رمزعبور",
"install_auth_username_enter": "نام کاربر را وارد کنید",
"install_auth_password_enter": "رمزعبور را وارد کنید",
"install_step": "گام",
"install_devices_title": "پیکربندی دستگاه شما",
"install_devices_desc": "به منظور اینکه AdGuard Home شروع به کار کند،باید دستگاه خود را برای استفاده از آن پیکربندی کنید.",
"install_submit_title": "تبریک می گوییم!",
"install_submit_desc": "روش راه اندازی به پایان رسیده و شما آماده استفاده از AdGuard Home هستید.",
"install_devices_router": "روتر",
"install_devices_router_desc": "این راه انداز خودکار همه دستگاه های متصل شده به روتر خانه را پوشش میدهد و نیازی نیست شما هر یک از آنها را دستی پیکربندی کنید.",
"install_devices_address": "DNS سرور AdGuard Home به آدرس های زیر گوش میدهد",
"install_devices_router_list_1": "اولویت ها را برای روتر خود باز کنید.معمولا میتوانید آن را ز طریق مرورگر از طریق آدرسی مانند ( http://192.168.0.1/ یا http://192.168.1.1/) دسترسی داشته باشید.ممکن است رمزعبور پرسیده شود،اگر آن را بخاطر ندارید،غالبا میتوان رمزعبور را با فشردن دکمه پشت روتر ریست کرد.برخی روترها برنامه خاصی نیاز دارد که باید در رایانه/گوشی نصب شده باشد.",
"install_devices_router_list_2": "تنظیمات DHCP/DNS را بیابید.دنبال حروف DNS بگردید در فیلدی که اجازه دو یا سه گروه عدد را میدهد و هر کدام در چهار گروه سه عددی شکسته شده است",
"install_devices_router_list_3": "آدرس سرور AdGuard Home خود را آنجا وارد کنید",
"install_devices_windows_list_1": "کنترل پنل را از طریق استارت منو یا جستجوی ویندوز باز کنید.",
"install_devices_windows_list_2": "بروید به شبکه و دسته اینترنت و سپس به شبکه و مرکز اشتراک گذاری",
"install_devices_windows_list_3": "در سمت چپ صفحه تنظیمات آداپتور را تغییر داده و روی آن کلیک کنید",
"install_devices_windows_list_4": "ارتباط فعال خود را انتخاب کرده،روی آن راست کلیک کرده و مشخصات را انتخاب کنید.",
"install_devices_windows_list_5": "پروتکل اینترنت نسخه 4 (TCP/IP) را در لیست بیابید،آن را انتخاب و سپس روی مشخصات دوباره کلیک کنید.",
"install_devices_windows_list_6": "گزینه استفاده از آدرس DNS سرور زیر را انتخاب کرده و آدرس سرور AdGuard Home خود را وارد کنید.",
"install_devices_macos_list_1": "روی آیکون اَپل کلیک کرده و بروید به اولویت های سیستم",
"install_devices_macos_list_2": "روی شبکه کلیک کنید",
"install_devices_macos_list_3": "اولین ارتباط را از لیست خود انتخاب و روی پیشرفته کلیک کنید.",
"install_devices_macos_list_4": "تب DNS را انتخاب و آدرس های سرور AdGuard Home خود را وارد کنید.",
"install_devices_android_list_1": "از منوی صفحه خانه آندروئید،تنظیمات را فشار دهید",
"install_devices_android_list_2": "وای فای را در منو فشار دهید،صفحه لیست کردن همه شبکه های موجود نشان داده میشود (تنظیم DNS دستی برای ارتباط موبایلی غیرممکن است)",
"install_devices_android_list_3": "به شبکه ای که متصل شده اید فشار طولانی دهید و ویرایش شبکه را انتخاب کنید.",
"install_devices_android_list_4": "در برخی دستگاه ها،شما ممکن است کادر پیشرفته را برای تنظیمات بعدی بررسی کنید.برای تنظیم DNS آندروئید خود،نیاز است شما از تنظیمات IP را از DHCP به Staticتغییر دهید.",
"install_devices_android_list_5": "گروه مقادیر DNS 1 و DNS 2 را به آدرس سرور AdGuard Home خود تغییر دهید.",
"install_devices_ios_list_1": "از صفحه خانه،تنظیمات را فشار دهید.",
"install_devices_ios_list_2": "وای فای را از منوی چپ انتخاب کنید (پیکربندی DNS دستی برای ارتباط موبایلی غیرممکن است).",
"install_devices_ios_list_3": "روی نام شبکه فعال فعلی کلیک کنید.",
"install_devices_ios_list_4": "در فیلد DNS آدرس سرور AdGuard Home را وارد کنید",
"get_started": "شروع به کار",
"next": "بعدی",
"open_dashboard": "بازکردن داشبورد",
"install_saved": "با موفقیت ذخیره نشد",
"encryption_title": "رمزگُذاری",
"encryption_desc": "پشتیبانی رمزگُذاری (HTTPS/TLS) برای DNS و رابط آدمین وب",
"encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد",
"encryption_server": "نام سرور",
"encryption_server_enter": "نام دامنه خود را وارد کنید",
"encryption_server_desc": "به منظور استفاده از HTTPS،شما باید نام سرور مطابق با گواهینامه اِس اِس اِل را وارد کنید.",
"encryption_redirect": "تغییر مسیر خودکار به HTTPS",
"encryption_redirect_desc": "اگر انتخاب شده باشد،AdGuard Home خودکار شما را از آدرس HTTP به HTTPS منتقل می کند",
"encryption_https": "پورت HTTPS",
"encryption_https_desc": "اگر پورت HTTPS پیکربندی شده باشد،رابط آدمین AdGuard Home از طریق HTTPS قابل دسترسی خواهد بود و آن همچنین DNS-over-HTTPS را در مکان '/dns-query' ارائه می دهد.",
"encryption_dot": "پورت DNS-over-TLS",
"encryption_dot_desc": "اگر این پورت پیکربندی شده باشد،AdGuard Home یک DNS-over-TLS سرور روی این پورت اجرا می کند",
"encryption_certificates": "گواهینامه ها",
"encryption_certificates_desc": "به منظور استفاده از رمزگُذاری، شما باید زنجیره گواهینامه اِس اِس اِل معتبر برای دامنه خود ارائه دهید. میتوانید گواهینامه رایگان از <0>{{link}}</0> بگیرید یا میتوانید آن را از مراجع گواهینامه معتبر بخرید.",
"encryption_certificates_input": "کپی/چسباندن گواهینامه پی ای اِم کد گذاری شده در اینجا.",
"encryption_status": "وضعیت",
"encryption_expire": "تاریخ انقضاء",
"encryption_key": "کلید خصوصی",
"encryption_key_input": "کپی/چسباندن کلید گواهینامه پی ای اِم کد گذاری شده در اینجا.",
"encryption_enable": "فعالسازی رمزگُذاری (HTTPS, DNS-over-HTTPS, و DNS-over-TLS)",
"encryption_enable_desc": "اگر رمزگُذاري فعال شده باشد،رابط آدمین AdGuard Home روی HTTPS کار خواهد کرد،و سرور DNS به درخواست های روی DNS-over-HTTPS و DNS-over-TLS گوش خواهد داد.",
"encryption_chain_valid": "زنجیره گواهینامه معتبر است",
"encryption_chain_invalid": "زنجیره گواهینامه نامعتبر است",
"encryption_key_valid": "این یک کلید خصوصی {{type}} معتبر است",
"encryption_key_invalid": "این یک کلید خصوصی {{type}} نامعتبر است",
"encryption_subject": "موضوع",
"encryption_issuer": "صادر کننده",
"encryption_hostnames": "نام میزبان",
"encryption_reset": "آیا میخواهید تنظیمات رمزگُذاری به پیش فرض بازگردد؟",
"topline_expiring_certificate": "گواهینامه اِس اِس اِل شما در صدد انقضاء است. <0>تنظیمات رمزگُذاری</0> را بروز رسانی کنید.",
"topline_expired_certificate": "گواهینامه اِس اِس اِل شما منقضی شده است. <0>تنظیمات رمزگُذاری</0> را بروز رسانی کنید.",
"form_error_port_range": "مقدار پورت را در محدوده 80-65535 وارد کنید",
"form_error_port_unsafe": "این پورت غیر ایمن است",
"form_error_equal": "نباید برابر باشد",
"form_error_password": "رمزعبور تطبیق ندارد",
"reset_settings": "ریست تنظیمات",
"update_announcement": "AdGuard Home {{version}} در دسترس است! <0>اینجا کلیک</0> کنید برای اطلاعات بیشتر.",
"setup_guide": "راهنمای راه اندازی",
"dns_addresses": "آدرس های DNS",
"down": "کار نمی کند",
"fix": "تعمیر",
"dns_providers": "در اینجا یک <0>لیست از سرویس های ارائه دهنده DNS</0> برای انتخاب هست.",
"update_now": "حالا بروز رسانی",
"update_failed": "بروز رسانی خودکار موفق نشد. لطفا <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>مراحل را دنبال کرده</a> تا بطور دستی بروز رسانی کنید.",
"processing_update": "منتظر بمانید،AdGuard Home در حال بروز رسانی است",
"clients_title": "کلاینت ها",
"clients_desc": "پیکربندی دستگاه های متصل شده به AdGuard Home",
"settings_global": "سراسری",
"settings_custom": "دستي",
"table_client": "کلاینت",
"table_name": "نام",
"save_btn": "ذخیره",
"client_add": "افزودن کلاینت",
"client_new": "کلاینت جدید",
"client_edit": "ویرایش کلاینت",
"client_identifier": "احراز با",
"ip_address": "آدرس آی پی",
"form_enter_ip": "آی پی را وارد کنید",
"form_enter_mac": "مَک را وارد کنید",
"form_client_name": "نام کلاینت را وارد کنید",
"client_global_settings": "استفاده از تنظیمات سراسری",
"client_deleted": "کلاینت \"{{key}}\" را با موفقیت حذف کرد",
"client_added": "کلاینت \"{{key}}\" را با موفقیت اضافه کرد",
"client_updated": "کلاینت \"{{key}}\" با موفقیت بروز رسانی شد",
"clients_not_found": "کلاینتی یافت نشد",
"client_confirm_delete": "آیا واقعا میخواهید \"{{key}}\" کلاینت را حذف کنید؟",
"filter_confirm_delete": "آیا واقعا میخواهید فیلتر را حذف کنید؟",
"auto_clients_title": "کلاینت ها (زمان اِجرا)",
"auto_clients_desc": "داده در کلاینت ها که از AdGuard Home استفاده می کند،اما در پیکربندی ذخیره نمی شود",
"access_title": "تنظیمات دسترسی",
"access_desc": "در اینجا میتوانید دستورات دسترسی را برای DNS سرور AdGuard Home وارد کنید.",
"access_allowed_title": "کلاینت های مجاز",
"access_allowed_desc": "یک لیست از CIDR یا آدرس های IP.اگر پیکربندی شود،AdGuard Home درخواست ها را فقط از این آدرس ها می پذیرد.",
"access_disallowed_title": "کلاینت های غیرمجاز",
"access_disallowed_desc": "یک لیست از CIDR یا آدرس های IP.اگر پیکربندی شود،AdGuard Home درخواست ها را از این آدرس های IP نمی پذیرد.",
"access_blocked_title": "دامنه های مسدود شده",
"access_blocked_desc": "این را با فیلتر ها به اشتباه نگیرید.AdGuard Home جستار DNS را با این دامنه ها در جستار سوال ها نمی پذیرد.",
"access_settings_saved": "تنظیمات دسترسی با موفقیت ذخیره شد",
"updates_checked": "بروز رسانی با موفقیت بررسی شد",
"updates_version_equal": "AdGuard Home بروز است",
"check_updates_now": "حالا بررسی برای بروز رسانی",
"dns_privacy": "حریم خصوصی DNS",
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> استفاده از<1>{{address}}</1> .",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> استفاده از <1>{{address}}</1> .",
"setup_dns_privacy_3": "<0>لطفا توجه کنید که پروتکل های رمزگذاری شده DNS فقط در آندروئید 9 پشتیبانی می شود. پس برای سیستم عامل های دیگر نیاز است که برنامه دیگری نصب کنید.</0><0>در اینجا میتوانید لیست نرم افزارهای قابل استفاده را ببینید.</0>",
"setup_dns_privacy_android_1": "آندروئید 9 بطور پیش فرض از DNS-over-TLS پشتیبانی می کند. برای پیکربندی آن، بروید به تنظیمات → شبکه & اینترنت → پیشرفته → DNS خصوصی و نام دامنه را آنجا وارد کنید.",
"setup_dns_privacy_android_2": "<0>AdGuard for Android</0> پشتیبانی از <1>DNS-over-HTTPS</1> و <1>DNS-over-TLS</1>.",
"setup_dns_privacy_android_3": "<0>Intra</0> قابلیت <1>DNS-over-HTTPS</1> را به آندروئید اضافه می کند.",
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> پشتیبانی از <1>DNS-over-HTTPS</1>, اما بمنظور پیکربندی آن برای استفاده بعنوان سرور خود،شما نیاز دارید که <2>DNS Stamp</2> برای آن تولید کنید.",
"setup_dns_privacy_ios_2": "<0>AdGuard for iOS</0> پشتیبانی از <1>DNS-over-HTTPS</1> و راه اندازی <1>DNS-over-TLS</1> .",
"setup_dns_privacy_other_title": "سایر راه کارها",
"setup_dns_privacy_other_1": "AdGuard Home خودش میتواند کلاینت DNS اَمن را در هر سیستم عاملی پیاده کند.",
"setup_dns_privacy_other_2": "<0>dnsproxy</0> از همه پروتکل های DNS شناخته شده پشتیبانی می کند.",
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supportsپشتیبانی از <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> پشتیبانی از <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "شما میتوانید راه کارهای بیشتر را در <0>اینجا</0> و <1>اینجا</1> پیدا کنید.",
"setup_dns_notice": "به منظور استفاده از <1>DNS-over-HTTPS</1> یا <1>DNS-over-TLS</1>، شما نیاز به <0>پیکربندی رمزگذاری</0> در تنظیمات AdGuard Home دارید.",
"rewrite_added": "بازنویسی DNS برای \"{{key}}\" با موفقیت اضافه شد",
"rewrite_deleted": "بازنویسی DNS برای \"{{key}}\" با موفقیت حذف شد",
"rewrite_add": "افزودن بازنویسی DNS",
"rewrite_not_found": "بازنویسی DNS یافت نشد",
"rewrite_confirm_delete": "آیا واقعا میخواهید بازنویسی DNS برای \"{{key}}\" را حذف کنید؟",
"rewrite_desc": "به آسانی اجازه پیکربندی پاسخ DNS دستی برای یک نام دامنه خاص را می دهد.",
"rewrite_applied": "دستور بازنویسی اِعمال شد",
"dns_rewrites": "بازنویسی های DNS",
"form_domain": "نام دامنه را وارد کنید",
"form_answer": "نام دامنه یا آدرس آی پی را وارد کنید",
"form_error_domain_format": "فرمت دامنه اشتباه است",
"form_error_answer_format": "فرمت پاسخ اشتباه است",
"configure": "پیکربندی",
"main_settings": "تنظیمات اصلی",
"block_services": "مسدودسازی سرویس های خاص",
"blocked_services": "سرویس های مسدود شده",
"blocked_services_desc": "مسدودسازی سریع سایت های عمومی و سرویس ها را اجازه می دهد.",
"blocked_services_saved": "سرویس های مسدود شده با موفقیت ذخیره شد",
"blocked_services_global": "از سرویس های مسدود شده سراسری استفاده کن",
"blocked_service": "سرویس مسدود شده",
"block_all": "مسدودسازی همه",
"unblock_all": "گشودن همه",
"encryption_certificate_path": "مسیر گواهینامه",
"encryption_private_key_path": "مسیر کلید خصوصی",
"encryption_certificates_source_path": "تنظیم مسیر فایل گواهینامه",
"encryption_certificates_source_content": "چسباندن محتوای گواهینامه",
"encryption_key_source_path": "تنظیم فایل کلید خصوصی",
"encryption_key_source_content": "چسباندن محتوای کلید خصوصی",
"stats_params": "پیکربندی آمار",
"config_successfully_saved": "پیکربندی با موفقیت ذخیره شد",
"interval_24_hour": "24 ساعت",
"interval_days": "{{value}} روز",
"interval_days_plural": "{{count}} روز",
"domain": "دامنه",
"answer": "پاسخ",
"filter_added_successfully": "فیلتر با موفقیت اضافه شد",
"statistics_retention": "مدت حفظ آمارها",
"statistics_retention_desc": "اگر مقدار فاصله را کاهش دهید،برخی داده ها از بین خواهد رفت",
"statistics_clear": " پاکسازی آمار",
"statistics_clear_confirm": "آیا واقعا میخواهید آمار را پاک کنید؟",
"statistics_cleared": "آمارها با موفقیت حذف شد"
}

View File

@@ -16,9 +16,7 @@
"dhcp_found": "Il y a plusieurs serveurs DHCP actifs sur le réseau. Ce n'est pas prudent d'activer le serveur DHCP intégré en ce moment.",
"dhcp_leases": "Locations des serveurs DHCP",
"dhcp_leases_not_found": "Aucune location des serveurs DHCP trouvée",
"dhcp_config_saved": "La configuration du serveur DHCP est sauvegardée",
"form_error_required": "Champ requis",
"form_error_ip_format": "Format IPv4 invalide",
"form_error_positive": "Doit être supérieur à 0",
"dhcp_form_gateway_input": "IP de la passerelle",
"dhcp_form_subnet_input": "Masque de sous-réseau",
@@ -73,11 +71,9 @@
"use_adguard_parental": "Utiliser le contrôle parental d'AdGuard",
"use_adguard_parental_hint": "AdGuard Home va vérifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du même API discret que celui utilisé par le service de Sécurité de navigation.",
"enforce_safe_search": "Renforcer la recherche sécurisée",
"enforce_save_search_hint": "AdGuard Home peut renforcer la Recherche sécurisée dans les moteurs de recherche suivants : Google, Youtube, Bing et Yandex.",
"no_servers_specified": "Pas de serveurs spécifiés",
"general_settings": "Paramètres généraux",
"upstream_dns": "Serveurs DNS upstream",
"upstream_dns_hint": "Si vous laissez ce champ vide, AdGuard Home va utiliser <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> somme upstream. Utilisez le préfixe tls:// pour DNS via les serveurs TLS .",
"test_upstream_btn": "Tester les upstreams",
"apply_btn": "Appliquer",
"disabled_filtering_toast": "Filtrage désactivé",
@@ -120,7 +116,7 @@
"example_upstream_tcp": "DNS classique (au-dessus de TCP)",
"all_filters_up_to_date_toast": "Tous les filtres sont mis à jour",
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis à jour",
"dns_test_ok_toast": "Les serveurs DNS spécifiés fonctionnent de manière incorrecte",
"dns_test_ok_toast": "Les serveurs DNS spécifiés fonctionnent de manière correcte",
"dns_test_not_ok_toast": "Impossible d'utiliser le serveur \"{{key}}\": veuillez vérifier si le nom saisi est bien correct",
"unblock_btn": "Débloquer",
"block_btn": "Bloquer",
@@ -160,4 +156,4 @@
"install_devices_windows_list_4": "Sélectionnez votre connexion active, clic droit dessus et sélectionnez Propriétés.",
"install_devices_windows_list_5": "Recherchez la version du protocole Internet 4 (TCP/IP) dans la liste, sélectionnez-la puis cliquez à nouveau sur Propriétés.",
"updates_version_equal": "AdGuard Home est à jour"
}
}

View File

@@ -0,0 +1,413 @@
{
"client_settings": "Postavke klijenta",
"example_upstream_reserved": "možete odrediti DNS upstream-ove <0>za određene domene</0>",
"upstream_parallel": "Koristi paralelne upite kako bi ubrzali rješavanje istovremenim ispitavanjem svih udaljenih poslužitelja",
"bootstrap_dns": "Bootstrap DNS poslužitelji",
"bootstrap_dns_desc": "Bootstrap DNS poslužitelji koriste se za rezolvanje IP adresa DoH/DoT rezolvera koje navedete kao upstreams.",
"check_dhcp_servers": "Provjera DHCP poslužitelja",
"save_config": "Spremi konfiguraciju",
"enabled_dhcp": "DHCP poslužitelj je omogućen",
"disabled_dhcp": "DHCP poslužitelj je onemogućen",
"dhcp_title": "DHCP poslužitelj (eksperimentalno!)",
"dhcp_description": "Ukoliko vaš router ne pruža DHCP postavke, možete koristiti AdGuardov ugrađeni DHCP poslužitelj.",
"dhcp_enable": "Omogući DHCP poslužitelj",
"dhcp_disable": "Onemogući DHCP poslužitelj",
"dhcp_not_found": "Sigurno je omogućiti ugrađeni DHCP poslužitelj - nismo pronašli aktivne DHCP poslužitelje na mreži. Međutim, preporučujemo vam da ponovo provjerite ručno, jer naš automatski test trenutno ne daje 100% jamstvo.",
"dhcp_found": "Aktivni DHCP poslužitelj je pronađen na mreži. Nije sigurno omogućiti ugrađeni DHCP poslužitelj.",
"dhcp_leases": "DHCP leases",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "Nisu pronađeni DHCP leases",
"dhcp_config_saved": "Postavke DHCP poslužitelja su uspješno spremljene",
"form_error_required": "Obavezno polje",
"form_error_ip4_format": "Nevažeći IPv4 format",
"form_error_ip6_format": "Nevažeći IPv6 format",
"form_error_ip_format": "Nevažeći format IP adrese",
"form_error_mac_format": "Nevažeći MAC format",
"form_error_positive": "Mora biti veće od 0",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet maskiranje",
"dhcp_form_range_title": "Raspon IP adresa",
"dhcp_form_range_start": "Početak raspona",
"dhcp_form_range_end": "Kraj raspona",
"dhcp_form_lease_title": "DHCP lease vrijeme (u sekundama)",
"dhcp_form_lease_input": "Lease trajanje",
"dhcp_interface_select": "Odaberite DHCP sučelje",
"dhcp_hardware_address": "Adresa hardvera",
"dhcp_ip_addresses": "IP adrese",
"dhcp_table_hostname": "Naziv računala",
"dhcp_table_expires": "Istječe",
"dhcp_warning": "Ako svejedno želite omogućiti DHCP poslužitelj, provjerite da nema drugog aktivnog DHCP poslužitelja na vašoj mreži. Inače može pokvariti Internet za ostale povezane uređaje!",
"dhcp_error": "Nismo mogli utvrditi postoji li drugi DHCP poslužitelj na mreži.",
"dhcp_static_ip_error": "Za korištenje DHCP poslužitelja mora se postaviti statička IP adresa. Nismo uspjeli utvrditi je li to mrežno sučelje postavljeno pomoću statičke IP adrese. Ručno postavite statičku IP adresu.",
"dhcp_dynamic_ip_found": "Vaš sustav koristi postavke dinamičkueIP adrese za sučelje <0>{{interfaceName}}</0>. Za korištenje DHCP poslužitelja mora se postaviti statička IP adresa. Vaša trenutna IP adresa je <0>{{ipAddress}}</0>. Ovu ćemo IP adresu automatski postaviti kao statičku ako pritisnete Omogući DHCP dugme.",
"dhcp_lease_added": "Statični lease \"{{key}}\" je uspješno dodan",
"dhcp_lease_deleted": "Statični lease \"{{key}}\" je uspješno uklonjen",
"dhcp_new_static_lease": "Novi static lease",
"dhcp_static_leases_not_found": "Nisu pronađeni statični DHCP leases",
"dhcp_add_static_lease": "Dodaj static lease",
"dhcp_reset": "Jeste li sigurni da želite poništiti DHCP postavke?",
"delete_confirm": "Jeste li sigurni da želite ukloniti \"{{key}}\"?",
"form_enter_hostname": "Unesite naziv računala",
"error_details": "Detalji o pogrešci",
"back": "Natrag",
"dashboard": "Upravljačka ploča",
"settings": "Postavke",
"filters": "Filtri",
"query_log": "Zapisnik upita",
"faq": "ČPP",
"version": "Verzija",
"address": "adresa",
"on": "UKLJUČENO",
"off": "ISKLJUČENO",
"copyright": "Autorsko pravo",
"homepage": "Početna stranica",
"report_an_issue": "Prijavite problem",
"privacy_policy": "Politika privatnosti",
"enable_protection": "Omogući zaštitu",
"enabled_protection": "Omogućena zaštita",
"disable_protection": "Onemogućena zaštita",
"disabled_protection": "Onemogućena zaštita",
"refresh_statics": "Osvježi statistiku",
"dns_query": "DNS Upiti",
"blocked_by": "<0>Blokirano filtrima</0>",
"stats_malware_phishing": "Blokiran zločudni softver/krađe identiteta",
"stats_adult": "Blokirane web stranice za odrasle",
"stats_query_domain": "Top tražene domene",
"for_last_24_hours": "u zadnja 24 sata",
"for_last_days": "zadnjih {{count}} dana",
"for_last_days_plural": "zadnjih {{count}} dana",
"no_domains_found": "Nije pronađena domena",
"requests_count": "Broj zahtjeva",
"top_blocked_domains": "Top blokirane domene",
"top_clients": "Top klijenti",
"no_clients_found": "Nema pronađenih klijenata",
"general_statistics": "Opća statistika",
"number_of_dns_query_days": "Broj DNS upita obrađenih u posljednja {{count}} dana",
"number_of_dns_query_days_plural": "Broj DNS upita obrađenih u posljednja {{count}} dana",
"number_of_dns_query_24_hours": "Broj DNS upita obrađenih u posljednja 24 sata",
"number_of_dns_query_blocked_24_hours": "Broj DNS zahtjeva koji su blokirani od strane filtara za blokiranje oglasa i lista neželjenih poslužitelja",
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahtjeva koje je blokirao modul AdGuard zaštita pregledavanja",
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih stranica s sadržajem za odrasle",
"enforced_save_search": "Omogućeno sigurno pretraživanje",
"number_of_dns_query_to_safe_search": "Broj DNS zahtjeva prema pretraživačima za koje je omogućeno Sigurno pretraživanje",
"average_processing_time": "Prosječno vrijeme obrade",
"average_processing_time_hint": "Prosječno vrijeme u milisekundama za obradu DNS zahtjeva",
"block_domain_use_filters_and_hosts": "Blokiraj domene koristeći filtre ili hosts datoteke",
"filters_block_toggle_hint": "Pravila blokiranja možete postaviti u postavkama <a href='#filters'>filtara</a>.",
"use_adguard_browsing_sec": "Koristi AdGuard uslugu zaštite pregledavanja",
"use_adguard_browsing_sec_hint": "AdGuard Home će provjeriti nalazi li se domena na popisu neželjenih domena od usluge zaštite pregledavanja. Za provjeru će se koristiti API za provjeru koji poštuje vašu privatnost. Samo mali dio SHA256 hash-a odnaziva domene se šalje poslužitelju.",
"use_adguard_parental": "Koristi web uslugu AdGuard roditeljske zaštite",
"use_adguard_parental_hint": "AdGuard Home provjeriti će sadrži li domena sadržaj za odrasle. Koristi isti API za zaštitu privatnosti kao i naša usluga zaštite pregledavanja.",
"enforce_safe_search": "Omogući sigurno pretraživanje",
"enforce_save_search_hint": "AdGuard Home može nametnuti sigurno pretraživanje na sljedećim tražilicama: Google, Youtube, Bing, DuckDuckGo i Yandex.",
"no_servers_specified": "Nije odabran nijedan poslužitelj",
"general_settings": "Opće postavke",
"dns_settings": "DNS postavke",
"encryption_settings": "Postavke šifriranja",
"dhcp_settings": "DHCP postavke",
"upstream_dns": "Upstream DNS poslužitelji",
"upstream_dns_hint": "Ako se ovo polje ostavi prazno, AdGuard Home će koristiti <a href='https://www.quad9.net/' target='_blank'>Quad9</a> kao upstream.",
"test_upstream_btn": "Testiraj upstream-ove",
"upstreams": "Upstreams",
"apply_btn": "Primijeni",
"disabled_filtering_toast": "Onemogućeno filtriranje",
"enabled_filtering_toast": "Omogućeno filtriranje",
"disabled_safe_browsing_toast": "Onemogućeno sigurno pretraživanje",
"enabled_safe_browsing_toast": "Omogućeno sigurno pretraživanje",
"disabled_parental_toast": "Onemogućen roditeljski nadzor",
"enabled_parental_toast": "Omogućen roditeljski nadzor",
"disabled_safe_search_toast": "Onemogućeno sigurno pretraživanje",
"enabled_save_search_toast": "Omogućeno sigurno pretraživanje",
"enabled_table_header": "Omogućeno",
"name_table_header": "Naziv",
"filter_url_table_header": "URL filtra",
"rules_count_table_header": "Broj pravila",
"last_time_updated_table_header": "Zadnje ažurirano",
"actions_table_header": "Radnje",
"edit_table_action": "Uredi",
"delete_table_action": "Ukloni",
"filters_and_hosts": "Filtri i lista neželjenih poslužitelja",
"filters_and_hosts_hint": "AdGuard Home razumije osnovna pravila blokiranja oglasa i sintaksu hosts datoteka.",
"no_filters_added": "Nema dodanih filtara",
"add_filter_btn": "Dodaj filtar",
"cancel_btn": "Poništi",
"enter_name_hint": "Unesite naziv",
"enter_url_hint": "Unesite URL",
"check_updates_btn": "Provjeri ažuriranja",
"new_filter_btn": "Pretplata na novi filtar",
"enter_valid_filter_url": "Unesite valjani URL za pretplatu filtra ili za hosts datoteku.",
"custom_filter_rules": "Prilagođena pravila filtriranja",
"custom_filter_rules_hint": "Unesite jedno pravilo po liniji. Možete koristiti sintaksu za pravila blokiranja oglasa ili za hosts datoteke.",
"examples_title": "Primjeri",
"example_meaning_filter_block": "blokira pristup domeni example.org kao i svim njenim poddomenama",
"example_meaning_filter_whitelist": "odblokira pristup domeni example.org kao i svim njenim poddomenama",
"example_meaning_host_block": "AdGuard Home će sada vratiti 127.0.0.1 adresu na example.org domenu (ali ne i poddomene).",
"example_comment": "! Ovdje ide komentar",
"example_comment_meaning": "samo komentar",
"example_comment_hash": "# Također komentar",
"example_regex_meaning": "blokira pristup domenama koje se podudaraju s regularnim izrazom",
"example_upstream_regular": "zadani DNS (putem UDP)",
"example_upstream_dot": "šifrirano <0>DNS-over-TLS</0>",
"example_upstream_doh": "šifrirano <0>DNS-over-HTTPS</0>",
"example_upstream_sdns": "možete koristiti <0>DNS Stamps</0> za <1>DNSCrypt</1> ili <2>DNS-over-HTTPS</2> rezolvere",
"example_upstream_tcp": "zadani DNS (putem TCP)",
"all_filters_up_to_date_toast": "Svi filtri su ažurirani",
"updated_upstream_dns_toast": "Ažurirani su upstream DNS poslužitelji",
"dns_test_ok_toast": "Odabrani DNS poslužitelji su trenutno aktivni",
"dns_test_not_ok_toast": "\"{{key}}\" poslužitelja: ne može se upotrijebiti, provjerite jeste li to ispravno napisali",
"unblock_btn": "Odblokiraj",
"block_btn": "Blokiraj",
"time_table_header": "Vrijeme",
"domain_name_table_header": "Naziv domene",
"type_table_header": "Vrsta",
"response_table_header": "Odgovor",
"client_table_header": "Klijent",
"empty_response_status": "Prazno",
"show_all_filter_type": "Prikaži sve",
"show_filtered_type": "Prikaži filtrirano",
"no_logs_found": "Nema zapisa",
"refresh_btn": "Osvježi",
"previous_btn": "Prethodno",
"next_btn": "Sljedeće",
"loading_table_status": "Učitavanje...",
"page_table_footer_text": "Stranica",
"of_table_footer_text": "od",
"rows_table_footer_text": "redova",
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja",
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja",
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrirao {{filter}}",
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",
"query_log_cleared": "Zapisnik upita je uspješno uklonjen",
"query_log_clear": "Očisti zapisnik upita",
"query_log_retention": "Spremanje zapisnika upita",
"query_log_enable": "Omogući zapise",
"query_log_configuration": "Postavke zapisa",
"query_log_disabled": "Zapisnik upita je onemogućen i može se postaviti u <0>postavkama</0>",
"query_log_strict_search": "Koristite dvostruke navodnike za strogo pretraživanje",
"query_log_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje zapisnika upita? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
"source_label": "Izvor",
"found_in_known_domain_db": "Pronađeno u bazi poznatih domena.",
"category_label": "Kategorija",
"rule_label": "Pravilo",
"filter_label": "Filtar",
"unknown_filter": "Nepoznati filtar {{filterId}}",
"install_welcome_title": "Dobrodošli u AdGuard Home!",
"install_welcome_desc": "AdGuard Home je DNS poslužitelj za blokiranje oglasa i pratitelja na cijeloj mreži. Njegova je svrha omogućiti vam upravljanje cijelom mrežom i svim svojim uređajima, a da to ne zahtijeva korištenje programa na strani klijenta.",
"install_settings_title": "Administratorsko web sučelje",
"install_settings_listen": "Osluškuj sučelje",
"install_settings_port": "Port",
"install_settings_interface_link": "Web administratorsko sučelje AdGuard Home-a će biti dostupno na sljedećim adresama:",
"form_error_port": "Unesite važeću vrijednost za port",
"install_settings_dns": "DNS poslužitelj",
"install_settings_dns_desc": "Potrebno je postaviti uređaj ili router da koristi DNS poslužitelj na sljedećim adresama:",
"install_settings_all_interfaces": "Sva sučelja",
"install_auth_title": "Autentikacija",
"install_auth_desc": "Izrazito se preporučuje postavljanje autentikacije za web administratorsko sučelje AdGuard Home. Iako je dostupna samo u vašoj lokalnoj mreži, važno je zaštititi je od ne dozvoljenog pristupa.",
"install_auth_username": "Korisničko ime",
"install_auth_password": "Lozinka",
"install_auth_confirm": "Potvrdi lozinku",
"install_auth_username_enter": "Unesite korisničko ime",
"install_auth_password_enter": "Unesite lozinku",
"install_step": "Korak",
"install_devices_title": "Postavite vaše uređaje",
"install_devices_desc": "Da biste započeli koristiti AdGuard Home, morate postaviti uređaje da ga koriste.",
"install_submit_title": "Čestitamo!",
"install_submit_desc": "Postavljanje je dovršeno i spremni ste koristiti AdGuard Home.",
"install_devices_router": "Usmjerivač (Router)",
"install_devices_router_desc": "Ovo postavljanje će automatski pokriti sve uređaje povezane na vaš kućni router i nećete trebati ručno postavljati svaki od njih.",
"install_devices_address": "AdGuard Home DNS poslužitelj osluškuje sljedeće adrese",
"install_devices_router_list_1": "Otvorite postavke za router. Obično mu možete pristupiti iz preglednika putem URL-a (kao što je http://192.168.0.1/ ili http://192.168.1.1/). Od vas će se možda tražiti da unesete lozinku. Ako je se ne sjećate, lozinku možete često poništiti pritiskom na dumge na samom routeru. Neki routeri trebaju određenu aplikaciju, koja bi u tom slučaju trebala biti već instalirana na vašem računalu/telefonu.",
"install_devices_router_list_2": "Pronađite DHCP/DNS postavke. Potražite DNS slova pored polja koje dopušta dva ili tri skupa brojeva, svaki razdvojen u četiri skupine od jedne do tri znamenke.",
"install_devices_router_list_3": "Unesite adresu AdGuard Home poslužitelja ovdje.",
"install_devices_windows_list_1": "Otvorite Upravljačku ploču putem Start izbornika ili Windows pretrage.",
"install_devices_windows_list_2": "Idite na kategoriju Mreža i Internet i odaberite Centar za mreže i zajedničko korištenje.",
"install_devices_windows_list_3": "Na lijevoj strani zaslona pronađite Promjeni postavke adaptera i pritisnite na to.",
"install_devices_windows_list_4": "Odaberite aktivnu vezu, pritisnite desni klik na nju i odaberite Svojstva.",
"install_devices_windows_list_5": "Pronađite Internet Protocol Version 4 (TCP/IP) na listi, odaberite ga i zatim pritisnite opet Postavke.",
"install_devices_windows_list_6": "Odaberite Koristi sljedeće DNS adrese poslužitelja i unesite vaše adrese od AdGuard Home poslužitelja.",
"install_devices_macos_list_1": "Pritisnite na Apple ikonu i idite u Postavke sustava.",
"install_devices_macos_list_2": "Pritisnite na Mreža.",
"install_devices_macos_list_3": "Odaberite prvu vezu s vašeg popisa i pritisnite Napredno.",
"install_devices_macos_list_4": "Odaberite DNS karticu i unesite adrese svog AdGuard Home poslužitelja.",
"install_devices_android_list_1": "Na početnom zaslonu Androida, odaberite Postavke.",
"install_devices_android_list_2": "Pritisnite Wi-Fi u izborniku. Prikazat će se zaslon s popisom svih dostupnih mreža (nemoguće je postaviti prilagođeni DNS za mobilnu vezu).",
"install_devices_android_list_3": "Dugo pritisnite na mrežu na koju ste povezani i odaberite Uredi mrežu.",
"install_devices_android_list_4": "Na nekim će uređajima možda trebati označiti Napredno za prikaz dodatnih postavki. Da biste prilagodili postavke Android DNS-a, morati će te prebaciti IP postavke s DHCP-a na Statičke.",
"install_devices_android_list_5": "Promijenite DNS 1 i DNS 2 vrijednosti u one adrese AdGuard Home poslužitelja.",
"install_devices_ios_list_1": "Na početnom zaslonu odaberite Postavke.",
"install_devices_ios_list_2": "Odaberite Wi-Fi u lijevom izborniku (ne moguće je postaviti DNS za mobilne mreže).",
"install_devices_ios_list_3": "Pritisnite na naziv vaše trenutne mreže.",
"install_devices_ios_list_4": "U DNS polje unesite adrese svog AdGuard Home poslužitelja.",
"get_started": "Započni",
"next": "Sljedeće",
"open_dashboard": "Otvori upravljačku ploču",
"install_saved": "Uspješno spremljeno",
"encryption_title": "Šifriranje",
"encryption_desc": "Podrška šifriranja (HTTPS/TLS) za DNS i administratorsko web sučelje",
"encryption_config_saved": "Spremljene postavke šifriranja",
"encryption_server": "Naziv poslužitelja",
"encryption_server_enter": "Unesite naziv domene",
"encryption_server_desc": "Kako biste koristili HTTPS, morate unijeti naziv poslužitelja koji odgovara vašem SSL certifikatu.",
"encryption_redirect": "Automatski preusmjeri na HTTPS",
"encryption_redirect_desc": "Ako je omogućeno, AdGuard Home će vas automatski preusmjeravati s HTTP na HTTPS adrese.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "Ako je HTTPS port postavljen, AdGuard Home administracijsko sučelje biti će dostupno putem HTTPS-a, a također će pružiti DNS-over-HTTPS na '/dns-query' lokaciji.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "Ako je ovaj port postavljen, AdGuard Home će pokrenuti DNS-over-TLS poslužitelj na ovom portu.",
"encryption_certificates": "Certifikati",
"encryption_certificates_desc": "Da biste koristili šifriranje, za svoju domenu morate osigurati važeći lanac SSL certifikata. Besplatan certifikat možete dobiti na <0>{{link}}</0> ili ga možete kupiti od jednog od pouzdanih izdavatelja certifikata.",
"encryption_certificates_input": "Zalijepite svoje PEM-kodirane certifikate ovdje.",
"encryption_status": "Status",
"encryption_expire": "Istječe",
"encryption_key": "Privatni ključ",
"encryption_key_input": "Zalijepite svoj PEM-kodiran privatni ključ certifikata ovdje.",
"encryption_enable": "Omogući šifriranje (HTTPS, DNS-over-HTTPS i DNS-over-TLS)",
"encryption_enable_desc": "Ako je šifriranje omogućeno, administratorsko sučelje AdGuard Home će raditi preko HTTPS-a, a DNS poslužitelj će osluškivati zahtjeve preko DNS-over-HTTPS i DNS-over-TLS.",
"encryption_chain_valid": "Lanac certifikata je valjan",
"encryption_chain_invalid": "Lanac certifikata nije valjan",
"encryption_key_valid": "Ovo je valjani {{type}} privatni ključ",
"encryption_key_invalid": "Ovo je nevažeći {{type}} privatni ključ",
"encryption_subject": "Predmet",
"encryption_issuer": "Izdavač",
"encryption_hostnames": "Nazivi računala",
"encryption_reset": "Jeste li sigurni da želite poništiti postavke šifriranja?",
"topline_expiring_certificate": "Vaš SSL certifikat uskoro ističe. Ažurirajte <0>Postavke šifriranja</0>.",
"topline_expired_certificate": "Vaš SSL certifikat je istekao. Ažurirajte <0>Postavke šifriranja</0>.",
"form_error_port_range": "Unesite vrijednost porta od 80 do 65536",
"form_error_port_unsafe": "Ovo je nesigurna port",
"form_error_equal": "Ne bi trebalo biti jednako",
"form_error_password": "Lozinka se ne podudara",
"reset_settings": "Poništi postavke",
"update_announcement": "AdGuard Home {{version}} je dostupan! <0>Pritisnite ovdje</0> za više informacija.",
"setup_guide": "Vodič za postavljanje",
"dns_addresses": "DNS adrese",
"dns_start": "Pokreće se DNS poslužitelj",
"dns_status_error": "Pogreška pri dohvatu statusa DNS poslužitelja",
"down": "Ne radi",
"fix": "Popravi",
"dns_providers": "Ovo je <0>popis poznatih DNS poslužitelja</0> za izbor.",
"update_now": "Ažuriraj sada",
"update_failed": "Ne uspješno automatsko ažuriranje. Molimo, <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>pratite ove korake</a> za ručno ažuriranje.",
"processing_update": "Molimo pričekajte, AdGuard Home se ažurira",
"clients_title": "Klijenti",
"clients_desc": "Postavite uređaje povezane na AdGuard Home",
"settings_global": "Globalno",
"settings_custom": "Prilagođeno",
"table_client": "Klijent",
"table_name": "Naziv",
"save_btn": "Spremi",
"client_add": "Dodaj klijenta",
"client_new": "Novi klijent",
"client_edit": "Uredi klijenta",
"client_identifier": "Identifikator",
"ip_address": "IP adresa",
"client_identifier_desc": "Klijenti se mogu prepoznati po IP adresi, CIDR-u ili MAC adresi. Imajte na umu da je upotreba MAC-a kao identifikatora, moguća samo ako je AdGuard Home također <0>DHCP poslužitelj</0>",
"form_enter_ip": "Unesite IP adresu",
"form_enter_mac": "Unesite MAC adresu",
"form_enter_id": "Unesi identifikator",
"form_add_id": "Dodaj identifikator",
"form_client_name": "Unesite naziv klijenta",
"client_global_settings": "Koristi globalne postavke",
"client_deleted": "Klijent \"{{key}}\" je uspješno uklonjen",
"client_added": "Klijent \"{{key}}\" je uspješno dodan",
"client_updated": "Klijent \"{{key}}\" je uspješno ažuriran",
"clients_not_found": "Nema pronađenih klijenata",
"client_confirm_delete": "Jeste li sigurni da želite ukloniti \"{{key}}\" klijenta?",
"filter_confirm_delete": "Jeste li sigurni da želite ukloniti ovaj filtar?",
"auto_clients_title": "Klijenti (runtime)",
"auto_clients_desc": "Podaci na klijentu koji koriste AdGuard Home, ali se ne spremaju u postavke",
"access_title": "Postavke pristupa",
"access_desc": "Postavite pravila pristupa za AdGuard Home DNS poslužitelj.",
"access_allowed_title": "Dopušteni klijenti",
"access_allowed_desc": "Popis CIDR-a ili IP adresa. Ukoliko je postavljeno, AdGuard Home će prihvatiti samo zahtjeve s ovih IP adresa.",
"access_disallowed_title": "Nedopušteni klijenti",
"access_disallowed_desc": "Popis CIDR-a ili IP adresa. Ukoliko je postavljeno, AdGuard Home će zaustaviti zahtjeve s ovih IP adresa.",
"access_blocked_title": "Blokirane domene",
"access_blocked_desc": "Ne miješajte ovo s filtrima. AdGuard Home će zaustaviti DNS upite s tim ovim domenama u podnesenim upitima.",
"access_settings_saved": "Postavke pristupa su uspješno spremljene",
"updates_checked": "Uspješna provjera ažuriranja",
"updates_version_equal": "AdGuard Home je ažuriran",
"check_updates_now": "Provjeri ažuriranja sada",
"dns_privacy": "DNS privatnost",
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> Koristite <1>{{address}}</1>.",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> Koristite <1>{{address}}</1>.",
"setup_dns_privacy_3": "<0>Imajte na umu da su šifrirani DNS protokoli podržani samo na Androidu 9. Stoga morate instalirati dodatni softver za ostale operativne sustave.</0><0>Evo popisa softvera koji možete koristiti.</0>",
"setup_dns_privacy_android_1": "Android 9 nativno podržava DNS-over-TLS. Da biste ga postavili, idite na Postavke → Mreža i internet → Napredno → Privatni DNS i tamo unesite svoje naziv domene.",
"setup_dns_privacy_android_2": "<0>AdGuard za Android</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.",
"setup_dns_privacy_android_3": "<0>Intra</0> dodaje <1>DNS-over-HTTPS</1> podršku za Android.",
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> podržava <1>DNS-over-HTTPS</1>, ali da biste ga postavili za upotrebu vašeg vlastitog poslužitelja, trebati će te generirati <2>DNS Stamp</2> za njega.",
"setup_dns_privacy_ios_2": "<0>AdGuard za iOS</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.",
"setup_dns_privacy_other_title": "Ostale implementacije",
"setup_dns_privacy_other_1": "AdGuard Home može poslužiti kao sigurni DNS klijent na svim platformama.",
"setup_dns_privacy_other_2": "<0>dnsproxy</0> podržava sve poznate sigurne DNS protokole.",
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podržava <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Možete pronaći više implementacija <0>ovdje</0> i <1>ovdje</1>.",
"setup_dns_notice": "Da biste koristili <1>DNS-over-HTTPS</1> ili <1>DNS-over-TLS</1>, morate <0>postaviti šifriranje</0> u AdGuard Home postavkama.",
"rewrite_added": "DNS prijepis za \"{{key}}\" je uspješno dodan",
"rewrite_deleted": "DNS prijepis za \"{{key}}\" je uspješno uklonjen",
"rewrite_add": "Dodaj DNS prijepis",
"rewrite_not_found": "Nema DNS prijepisa",
"rewrite_confirm_delete": "Jeste li sigurni da želite ukloniti DNS prijepis za \"{{key}}\" klijenta?",
"rewrite_desc": "Omogućuje jednostavno postavljanje prilagođenog DNS odgovora za određenu domenu.",
"rewrite_applied": "Primijenjena pravila prijepisa",
"dns_rewrites": "DNS prijepisi",
"form_domain": "Unesite domenu",
"form_answer": "Unesite IP adresu ili naziv domene",
"form_error_domain_format": "Nevažeći format domene",
"form_error_answer_format": "Nevažeći format odgovora",
"configure": "Konfiguriraj",
"main_settings": "Opće postavke",
"block_services": "Blokiraj specifične usluge",
"blocked_services": "Blokirane usluge",
"blocked_services_desc": "Omogućuje brzo blokiranje popularnih stranica i usluga.",
"blocked_services_saved": "Blokirane usluge su uspješno spremljene",
"blocked_services_global": "Koristi globalno blokirane usluge",
"blocked_service": "Blokirane usluge",
"block_all": "Blokiraj sve",
"unblock_all": "Odblokiraj sve",
"encryption_certificate_path": "Putanja certifikata",
"encryption_private_key_path": "Putanja privatnog ključa",
"encryption_certificates_source_path": "Dodajte datoteku certifikata",
"encryption_certificates_source_content": "Zalijepi sadržaj certifikata",
"encryption_key_source_path": "Dodajte datoteku privatnog ključa",
"encryption_key_source_content": "Zalijepi sadržaj privatnog ključa",
"stats_params": "Postavke statistike",
"config_successfully_saved": "Postavke su uspješno spremljene",
"interval_24_hour": "24 sata",
"interval_days": "{{count}} dan",
"interval_days_plural": "{{count}} dana",
"domain": "Domena",
"answer": "Odgovor",
"filter_added_successfully": "Filtar je uspješno dodan",
"statistics_configuration": "Postavke statistike",
"statistics_retention": "Spremanje statistike",
"statistics_retention_desc": "Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
"statistics_clear": " Poništi statistiku",
"statistics_clear_confirm": "Jeste li sigurni da želite poništiti statistiku?",
"statistics_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
"statistics_cleared": "Statistika je uspješno uklonjenja",
"interval_hours": "{{count}} sata/i",
"interval_hours_plural": "{{count}} sata/i",
"filters_configuration": "Postavke filtara",
"filters_enable": "Omogući filtre",
"filters_interval": "Interval ažuriranja filtara",
"disabled": "Onemogućeno",
"username_label": "Korisničko ime",
"username_placeholder": "Unesite korisničko ime",
"password_label": "Lozinka",
"password_placeholder": "Unesite lozinku",
"sign_in": "Prijava",
"sign_out": "Odjava",
"forgot_password": "Zaboravljena lozinka?",
"forgot_password_desc": "Slijedite <0>ove korake</0> da biste stvorili novu lozinku za svoj korisnički račun.",
"location": "Lokacija",
"orgname": "Naziv organizacije",
"netname": "Naziv mreže",
"descr": "Opis",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Saznajte više</0> o stvaranju vlastitog hosts popisa neželjenih.",
"blocked_by_response": "Blokirano od strane CNAME-a ili IP-a u odgovoru"
}

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP leases",
"dhcp_static_leases": "DHCP static leases",
"dhcp_leases_not_found": "DHCP lease tidak ditemukan",
"dhcp_config_saved": "Pengaturan server DHCP tersimpan",
"form_error_required": "Kolom yang harus diisi",
"form_error_ip_format": "Format IPv4 tidak valid",
"form_error_mac_format": "Format MAC tidak valid",
"form_error_positive": "Harus lebih dari 0",
"dhcp_form_gateway_input": "IP gateway",
@@ -96,14 +94,12 @@
"use_adguard_parental": "Gunakan layanan web kontrol orang tua AdGuard",
"use_adguard_parental_hint": "AdGuard Home akan mengecek jika domain mengandung materi dewasa. Akan menggunakan API yang ramah privasi yang sama sebagai layanan web keamanan penjelajahan.",
"enforce_safe_search": "Paksa penelusuran aman",
"enforce_save_search_hint": "AdGuard Home dapat memaksa penelusuran aman pada mesin pencari berikut: Google, Youtube, Bing, dan Yandex.",
"no_servers_specified": "Sever tidak disebutkan",
"general_settings": "Pengaturan umum",
"dns_settings": "Pengaturan DNS",
"encryption_settings": "Pengaturan enkripsi",
"dhcp_settings": "Pengaturan DHCP",
"upstream_dns": "Server DNS hulu",
"upstream_dns_hint": "Jika Anda mengosongkan kolom ini, AdGuard Home akan menggunakan <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> sebagai hulu. Gunakan tls:// untuk server DNS over TLS.",
"test_upstream_btn": "Uji hulu",
"apply_btn": "Terapkan",
"disabled_filtering_toast": "Penyaringan nonaktif",
@@ -141,7 +137,6 @@
"example_comment": "! Komentar di sini",
"example_comment_meaning": "hanya sebuah komentar",
"example_comment_hash": "Juga sebuah komentar",
"example_regex_meaning": "blokir akses ke domain yang cocok dengan ekspresi reguler yang ditentukan",
"example_upstream_regular": "DNS reguler (melalui UDP)",
"example_upstream_dot": "terenkripsi <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>",
"example_upstream_doh": "terenkripsi <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>",
@@ -287,7 +282,6 @@
"client_edit": "Ubah Klien",
"client_identifier": "Identifikasi",
"ip_address": "Alamat IP",
"client_identifier_desc": "Klien dapat diidentifikasi dengan alamat IP atau alamat MAC. Harap dicatat bahwa menggunakan MAC sebagai pengidentifikasi hanya dimungkinkan jika AdGuard Home juga merupakan <0>server DHCP</0>",
"form_enter_ip": "Masukkan IP",
"form_enter_mac": "Masukkan MAC",
"form_client_name": "Masukkan nama klien",

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "Leases DHCP",
"dhcp_static_leases": "Leases DHCP statici",
"dhcp_leases_not_found": "Nessun lease DHCP trovato",
"dhcp_config_saved": "Configurazione server DHCP salvata",
"form_error_required": "Campo richiesto",
"form_error_ip_format": "Formato IPv4 non valido",
"form_error_mac_format": "Formato MAC non valido",
"form_error_positive": "Deve essere maggiore di 0",
"dhcp_form_gateway_input": "IP Gateway",
@@ -96,14 +94,12 @@
"use_adguard_parental": "Usa il servizio parental control di AdGuard",
"use_adguard_parental_hint": "AdGuard Home controllerà se il dominio contiene materiale per adulti. Usa le stesse API privacy-friendly del servizio web browsing security.",
"enforce_safe_search": "Forza ricerca sicura",
"enforce_save_search_hint": "AdGuard Home può forzare la ricerca sicura sui seguenti motori di ricerca: Google, YouTube, Bing e Yandex",
"no_servers_specified": "Nessun server specificato",
"general_settings": "Impostazioni generali",
"dns_settings": "Impostazioni DNS",
"encryption_settings": "Impostazioni di criptazione",
"dhcp_settings": "Impostazioni DHCP",
"upstream_dns": "Server DNS upstream",
"upstream_dns_hint": "Se lasci vuoto questo campo, AdGuard Home imposterà i <a href='https://1.1.1.1/' target='_blank'>DNS di Cloudflare</a> come upstream. Inserisci il prefisso tls:// per i server con DNS over TLS",
"test_upstream_btn": "Testa gli upstream",
"apply_btn": "Applica",
"disabled_filtering_toast": "Disabilita filtri",
@@ -141,6 +137,7 @@
"example_comment": "! Qui va un commento",
"example_comment_meaning": "un commento",
"example_comment_hash": "# Un altro commento",
"example_regex_meaning": "blocca l'accesso ai domini che corrispondono alla specifica espressione regolare",
"example_upstream_regular": "DNS regolari (via UDP)",
"example_upstream_dot": "<a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a> criptato",
"example_upstream_doh": "<a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a> criptato",
@@ -172,6 +169,16 @@
"rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate",
"rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrato da {{filter}}",
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",
"query_log_cleared": "La query log è stata cancellata correttamente",
"query_log_clear": "Cancella query logs",
"query_log_retention": "Ritenzione query logs",
"query_log_enable": "Abilita log",
"query_log_configuration": "Configurazione logs",
"query_log_disabled": "La query log è stata disabilitata e può essere configurata nel <0>impostazioni</0>",
"query_log_strict_search": "Utilizzare le virgolette doppie per la ricerca rigorosa",
"query_log_retention_confirm": "Sei sicuro di voler modificare il registro di query? Se si diminuisce il valore di intervallo, alcuni dati saranno persi",
"source_label": "Fonte",
"found_in_known_domain_db": "Trovato nel database dei domini conosciuti.",
"category_label": "Categoria",
@@ -286,7 +293,6 @@
"client_edit": "Modifica Client",
"client_identifier": "Identificatore",
"ip_address": "Indirizzo IP",
"client_identifier_desc": "I client possono essere identificati dall indirizzo IP o dall' indirizzo MAC. Nota che l' utilizzo dell' indirizzo MAC come identificatore è consentito solo se AdGuard Home è anche il <0>server DHCP</0>",
"form_enter_ip": "Inserisci IP",
"form_enter_mac": "Inserisci MAC",
"form_client_name": "Inserisci nome client",
@@ -349,11 +355,45 @@
"blocked_service": "Servizio bloccato",
"block_all": "Blocca tutto",
"unblock_all": "Sblocca tutto",
"encryption_certificate_path": "Percorso di certificato",
"encryption_private_key_path": "Percorso della chiave privata",
"encryption_certificates_source_path": "Definisci un percorso alle file dei certificati",
"encryption_certificates_source_content": "Incolla i contenuti di certificato",
"encryption_key_source_path": "Imposta un file chiave privata",
"encryption_key_source_content": "Incolla i contenuti della chiave privata",
"stats_params": "Configurazione delle statistiche",
"config_successfully_saved": "Configurazione salvata correttamente",
"interval_24_hour": "24 ore",
"interval_days": "{{count}} giorni",
"interval_days_plural": "{{count}} giorni",
"domain": "Dominio",
"answer": "Risposta",
"filter_added_successfully": "Il filtro è stato aggiunto correttamente"
"filter_added_successfully": "Il filtro è stato aggiunto correttamente",
"statistics_configuration": "Configurazione delle statistiche",
"statistics_retention": "Conservazione statistiche",
"statistics_retention_desc": "Se si diminuisce il valore di intervallo, alcuni dati saranno persi",
"statistics_clear": " Azzera statistiche",
"statistics_clear_confirm": "Sei sicuro di voler azzerare le statistiche?",
"statistics_retention_confirm": "Sei sicuro di modificare la conservazione statistiche? Se si diminuisce il valore di intervallo, alcuni dati saranno persi",
"statistics_cleared": "Statistiche azzerate correttamente",
"interval_hours": "{{count}} ora",
"interval_hours_plural": "{{count}} ore",
"filters_configuration": "Configurazione filtri",
"filters_enable": "Abilita i filtri",
"filters_interval": "Intervallo aggiornamento filtri",
"disabled": "Disabilitato",
"username_label": "Nome utente",
"username_placeholder": "Inserisci nome utente",
"password_label": "Password",
"password_placeholder": "Inserisci password",
"sign_in": "Accedi",
"sign_out": "Esci",
"forgot_password": "Hai perso la password?",
"forgot_password_desc": "Per favore segui <0>questi punti</0> per creare una nuova password per il tuo account.",
"location": "Locazione",
"orgname": "Nome dell'organizzazione",
"netname": "Nome Network",
"descr": "Descrizione",
"whois": "Chi è",
"filtering_rules_learn_more": "<0>Impara di più</0> come creare i tuoi elenchi di blocco per i hosts."
}

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP割り当て",
"dhcp_static_leases": "DHCP静的割り当て",
"dhcp_leases_not_found": "DHCP割当はありません",
"dhcp_config_saved": "DHCPサーバの設定を保存しました",
"form_error_required": "必須項目",
"form_error_ip_format": "IPv4フォーマットではありません",
"form_error_mac_format": "MACフォーマットではありません",
"form_error_positive": "0より大きい必要があります",
"dhcp_form_gateway_input": "ゲートウェイIP",
@@ -90,14 +88,12 @@
"use_adguard_parental": "AdGuardペアレンタルコントロール・ウェブサービスを使用する",
"use_adguard_parental_hint": "AdGuard Homeは、ドメインにアダルトコンテンツが含まれているかどうかを確認します。 ブラウジングセキュリティ・ウェブサービスと同じプライバシーに優しいAPIを使用します。",
"enforce_safe_search": "セーフサーチを強制する",
"enforce_save_search_hint": "AdGuard Homeは、Google、Youtube、Bing、Yandexの検索エンジンでセーフサーチを強制できます。",
"no_servers_specified": "サーバが指定されていません",
"general_settings": "一般設定",
"dns_settings": "DNS設定",
"encryption_settings": "暗号化設定",
"dhcp_settings": "DHCP設定",
"upstream_dns": "上流DNSサーバ",
"upstream_dns_hint": "このフィールドを未入力のままにすると、AdGuard Homeは上流として<a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a>を使用します。DNS over TLSサーバには、「tls://」プレフィックスを使用してください。",
"test_upstream_btn": "上流サーバをテストする",
"apply_btn": "適用する",
"disabled_filtering_toast": "フィルタリングを無効にしました",
@@ -135,7 +131,6 @@
"example_comment": "! ここにはコメントが入ります",
"example_comment_meaning": "ただのコメントです",
"example_comment_hash": "# ここもコメントです",
"example_regex_meaning": "指定の正規表現に一致するドメインへのアクセスをブロックします",
"example_upstream_regular": "通常のDNSUDPでの問い合わせ",
"example_upstream_dot": "暗号化されている <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>",
"example_upstream_doh": "暗号化されている <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>",
@@ -279,7 +274,6 @@
"client_edit": "クライアントの編集",
"client_identifier": "識別子",
"ip_address": "IPアドレス",
"client_identifier_desc": "クライアントはIPアドレスまたはMACアドレスで識別できます。AdGuard Homeが<0>DHCPサーバ</0>でもある場合にのみ、識別子としてMACを使用することが可能であることにご注意ください。",
"form_enter_ip": "IPアドレスを入力してください",
"form_enter_mac": "MACアドレスを入力してください",
"form_client_name": "クライアント名を入力してください",

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP 임대",
"dhcp_static_leases": "DHCP 고정 임대",
"dhcp_leases_not_found": "DHCP 임대를 찾을 수 없음",
"dhcp_config_saved": "저장된 DHCP 서버 구성",
"form_error_required": "필수 필드",
"form_error_ip_format": "잘못된 IPv4 형식",
"form_error_mac_format": "잘못된 MAC 형식",
"form_error_positive": "0보다 커야 합니다",
"dhcp_form_gateway_input": "게이트웨이 IP",
@@ -96,14 +94,12 @@
"use_adguard_parental": "AdGuard 자녀 보호 웹 서비스 사용",
"use_adguard_parental_hint": "AdGuard Home은 도메인에 성인 자료가 포함되어 있는지 확인합니다. 브라우징 보안 웹 서비스와 동일한 개인정보 보호 API를 사용함.",
"enforce_safe_search": "세이프서치 강제",
"enforce_save_search_hint": "AdGuard Home은 다음과 같은 검색엔진(구글, 유투브, 빙, 덕덕고, 얀덱스)에서 안전검색이 가능합니다.",
"no_servers_specified": "지정된 서버 없음",
"general_settings": "일반 설정",
"dns_settings": "DNS 설정",
"encryption_settings": "암호화 설정",
"dhcp_settings": "DHCP 설정",
"upstream_dns": "업스트림 DNS 서버",
"upstream_dns_hint": "이 항목을 비워 두면 AdGuard Home에서 <a href='https://1.1.1.1/' target='_blank'> Cloudflare DNS </a>를 업스트림으로 사용합니다.",
"test_upstream_btn": "업스트림 테스트",
"apply_btn": "적용",
"disabled_filtering_toast": "필터링 비활성화됨",
@@ -297,7 +293,6 @@
"client_edit": "클라이언트 수정",
"client_identifier": "식별자",
"ip_address": "IP 주소",
"client_identifier_desc": "사용자는 IP 주소 또는 MAC 주소로 식별할 수 있지만 AdGuard Home이 <0>DHCP 서버인 </0> 경우에만 사용자는 MAC 주소로 식별할 수 있습니다.",
"form_enter_ip": "IP 입력",
"form_enter_mac": "MAC 입력",
"form_client_name": "클라이언트 이름 입력",
@@ -399,5 +394,6 @@
"orgname": "단체 이름",
"netname": "네트워크 이름",
"descr": "설명",
"whois": "후이즈"
"whois": "후이즈",
"filtering_rules_learn_more": "차단 리스트를 직접 호스트하는 법을 <0>알아보세요</0>."
}

View File

@@ -16,9 +16,9 @@
"dhcp_found": "Actieve DHCP server(s) gevonden op het netwerk. het is NIET veilig om de ingebouwde DHCP server in te schakelen.",
"dhcp_leases": "DHCP lease overzicht",
"dhcp_leases_not_found": "Geen DHCP lease gevonden",
"dhcp_config_saved": "DHCP server configuratie opgeslagen",
"form_error_required": "Vereist veld",
"form_error_ip_format": "Ongeldig IPv4 formaat",
"form_error_ip4_format": "Ongeldig IPv4 formaat",
"form_error_ip6_format": "Ongeldig IPv6 formaat",
"form_error_mac_format": "Ongeldig MAC formaat.",
"form_error_positive": "Moet groter zijn dan 0",
"dhcp_form_gateway_input": "Gateway IP",
@@ -37,6 +37,7 @@
"dhcp_new_static_lease": "Voeg static lease toe",
"dhcp_static_leases_not_found": "Geen DHCP static lease gevonden",
"dhcp_add_static_lease": "Voeg statische lease toe",
"dhcp_reset": "Weet je zeker dat je de DHCP configuratie wil resetten?",
"delete_confirm": "Ben je zeker dat je \"{{key}}\" wilt verwijderen?",
"form_enter_hostname": "Vul hostnaam in",
"error_details": "Fout details",
@@ -87,15 +88,14 @@
"use_adguard_parental": "Gebruik AdGuard Ouderlijk toezicht web service",
"use_adguard_parental_hint": "AdGuard Home controleert of het domein 18+ content bevat. Dit gebeurt dmv dezelfde privacy vriendelijke API als de Browsing Security web service.",
"enforce_safe_search": "Forceer Veilig Zoeken",
"enforce_save_search_hint": "AdGuard Home kan veilig zoeken forceren voor de volgende zoekmachines: Google, Youtube, Bing, en Yandex.",
"no_servers_specified": "Geen servers gespecificeerd",
"general_settings": "Generieke instellingen",
"dns_settings": "DNS Instellingen",
"encryption_settings": "Encryptie Instellingen",
"dhcp_settings": "DHCP Instellingen",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "Indien je dit veld leeg laat, zal AdGuard Home <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> gebruiken als upstream. Gebruik tls:// voor DNS over TLS servers.",
"test_upstream_btn": "Test upstream",
"upstreams": "Upstreams",
"apply_btn": "Toepassen",
"disabled_filtering_toast": "Filters uitgeschakeld",
"enabled_filtering_toast": "Filters ingeschakeld",
@@ -132,7 +132,6 @@
"example_comment": "! Hier komt een opmerking",
"example_comment_meaning": "zomaar een opmerking",
"example_comment_hash": "# Nog een opmerking",
"example_regex_meaning": "blokkeer de toegang tot de domeinen die overeenkomen met de opgegeven reguliere expressie",
"example_upstream_regular": "standaard DNS (over UDP)",
"example_upstream_dot": "versleutelde<a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>",
"example_upstream_doh": "versleutelde<a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>",
@@ -260,6 +259,8 @@
"update_announcement": "AdGuard Home{{version}} is nu beschikbaar! <0>klik hier</0> voor meer info.",
"setup_guide": "Installatie gids",
"dns_addresses": "DNS adressen",
"dns_start": "DNS server aan het opstarten",
"dns_status_error": "Fout bij het oproepen van de DNS server status",
"down": "Uitgeschakeld",
"fix": "Los op",
"dns_providers": "hier is een <0>lijst of gekende DNS providers</0> waarvan je kan kiezen.",
@@ -278,9 +279,10 @@
"client_edit": "Wijzig gebruiker",
"client_identifier": "Identificeer via",
"ip_address": "IP adres",
"client_identifier_desc": "Gebruikers kunnen worden geïdentificeerd door het IP-adres of MAC-adres. Houd er rekening mee dat het gebruik van MAC als ID alleen mogelijk is als AdGuard Home ook een <0> DHCP-server </0> is",
"form_enter_ip": "Vul IP in",
"form_enter_mac": "Vul MAC in",
"form_enter_id": "ID invoeren",
"form_add_id": "ID toevoegen",
"form_client_name": "Vul gebruikersnaam in",
"client_global_settings": "Gebruik globale instelling",
"client_deleted": "Gebruiker \"{{key}}\" met succes verwijderd",
@@ -340,5 +342,6 @@
"statistics_retention_desc": "Als je de interval waarde vermindert, zullen sommige gegevens verloren gaan",
"statistics_clear": " Statistieken wissen",
"statistics_clear_confirm": "Alle statistieken werkelijk wissen?",
"statistics_cleared": "Statistieken succesvol gewist"
"statistics_cleared": "Statistieken succesvol gewist",
"blocked_by_response": "Geblokkeerd door CNAME of IP als antwoord"
}

View File

@@ -0,0 +1,403 @@
{
"client_settings": "Klientinnstillinger",
"example_upstream_reserved": "du kan bestemme en oppstrøms-DNS <0>for et spesifikt domene(r)</0>",
"upstream_parallel": "Bruk parallele forespørsler for å få oppfarten på behandlinger, ved å forespørre til alle oppstrømstjenerne samtidig",
"bootstrap_dns": "Bootstrap-DNS-tjenere",
"bootstrap_dns_desc": "Bootstrap-DNS-tjenere brukes til å oppklare IP-adressene til DoH/DoT-oppklarerene som du har valgt som oppstrømstjenere.",
"check_dhcp_servers": "Se etter DHCP-tjenere",
"save_config": "Lagre oppsettet",
"enabled_dhcp": "DHCP-tjeneren ble skrudd på",
"disabled_dhcp": "DHCP-tjeneren ble skrudd av",
"dhcp_title": "DHCP-tjener (eksperimentell!)",
"dhcp_description": "Dersom ruteren din ikke har DHCP-innstillinger, kan du bruke AdGuard Home sin egen innebygde DHCP-tjener.",
"dhcp_enable": "Skru på DHCP-tjeneren",
"dhcp_disable": "Skru av DHCP-tjeneren",
"dhcp_not_found": "Det er trygt å skru på den innebygde DHCP-tjeneren - vi kunne ikke finne noen aktive DHCP-tjenere i nettverket. Men vi oppfordrer deg til å dobbeltsjekke manuelt, siden vår automatiske test ikke gir 100% sikre svar ennå.",
"dhcp_found": "En aktiv DHCP-tjener ble oppdaget i nettverket. Det er ikke trygt å bruke den innebygde DHCP-tjeneren.",
"dhcp_leases": "DHCP-leieavtaler",
"dhcp_static_leases": "Statiske DHCP-leieavtaler",
"dhcp_leases_not_found": "Ingen DHCP-leieavtaler ble funnet",
"form_error_required": "Påkrevd felt",
"form_error_ip4_format": "Ugyldig IPv4-format",
"form_error_ip6_format": "Ugyldig IPv6-format",
"form_error_ip_format": "Ugyldig IPv4-format",
"form_error_mac_format": "Ugyldig MAC-format",
"form_error_positive": "Må være høyere enn 0",
"dhcp_form_gateway_input": "Gateway-IP",
"dhcp_form_subnet_input": "Nettverksmaske",
"dhcp_form_range_title": "Spennvidden til IP-adressene",
"dhcp_form_range_start": "Rekkeviddestart",
"dhcp_form_range_end": "Rekkeviddeslutt",
"dhcp_form_lease_title": "DHCP-leieavtalevarighet (i sekunder)",
"dhcp_form_lease_input": "Leieavtalenes varighet",
"dhcp_interface_select": "Velg DHCP-grensesnitt",
"dhcp_hardware_address": "Maskinvareadresse",
"dhcp_ip_addresses": "IP-adresser",
"dhcp_table_hostname": "Vertsnavn",
"dhcp_table_expires": "Utløper",
"dhcp_warning": "Hvis du vil aktivere DHCP-tjeneren likevel, så sørg for at det ikke er noen andre aktive DHCP-tjenere i nettverket ditt. Ellers kan det knekke internettilgangen til tilkoblede enheter!",
"dhcp_error": "Vi klarte ikke å fastslå om det er en annen DHCP-tjener i nettverket ditt eller ikke.",
"dhcp_static_ip_error": "For å kunne bruke DHCP-tjeneren, må det være satt en statisk IP-adresse. Vi klarte ikke å finne ut om dette nettverksgrensesnittet har blitt satt opp med en statisk IP-adresse. Vennligst sett opp en statisk IP-adresse manuelt.",
"dhcp_dynamic_ip_found": "Systemet ditt bruker et oppsett med dynamisk IP-adresse for grensesnittet <0>{{interfaceName}}</0>. For å kunne bruke DHCP-tjeneren, må en statisk IP-adresse ha blitt satt opp. Din nåværende IP-adresse er <0>{{ipAddress}}</0>. Vi vil automatisk gjøre denne IP-adressen statisk hvis du trykker på «Skru på DHCP»-knappen.",
"dhcp_lease_added": "Den statiske leieavtalen «{{key}}» ble vellykket lagt til",
"dhcp_lease_deleted": "Den statiske leieavtalen «{{key}}» ble vellykket lagt slettet",
"dhcp_new_static_lease": "Ny statisk leieavtale",
"dhcp_static_leases_not_found": "Ingen statiske DHCP-leieavtaler ble funnet",
"dhcp_add_static_lease": "Legg til statisk leieavtale",
"delete_confirm": "Er du sikker på at du vil slette «{{key}}»?",
"form_enter_hostname": "Skriv inn vertsnavnet",
"error_details": "Feildetaljer",
"back": "Tilbake",
"dashboard": "Kontrollsenter",
"settings": "Innstillinger",
"filters": "Filtre",
"query_log": "Forespørselslogg",
"faq": "OSS",
"version": "Versjon",
"address": "adresse",
"on": "PÅ",
"off": "AV",
"copyright": "Opphavsrett",
"homepage": "Hjemmeside",
"report_an_issue": "Send inn feilrapport",
"privacy_policy": "Personvernretningslinjer",
"enable_protection": "Skru på beskyttelse",
"enabled_protection": "Beskyttelse ble skrudd på",
"disable_protection": "Skru av beskyttelse",
"disabled_protection": "Beskyttelsen ble skrudd av",
"refresh_statics": "Oppfrisk statistikkene",
"dns_query": "DNS-forespørsler",
"blocked_by": "<0>Blokkert av filtre</0>",
"stats_malware_phishing": "Blokkert skadevare/phishing",
"stats_adult": "Blokkerte voksennettsteder",
"stats_query_domain": "Mest forespurte domener",
"for_last_24_hours": "de siste 24 timene",
"for_last_days": "det siste døgnet",
"for_last_days_plural": "de siste {{count}} dagene",
"no_domains_found": "Ingen domener ble funnet",
"requests_count": "Antall forespørsler",
"top_blocked_domains": "Mest blokkerte domener",
"top_clients": "Vanligste klienter",
"no_clients_found": "Ingen klienter ble funnet",
"general_statistics": "Generelle statistikker",
"number_of_dns_query_days": "Antall DNS-forespørsler som ble behandlet det siste døgnet",
"number_of_dns_query_days_plural": "Antall DNS-forespørsler som ble behandlet de siste {{count}} dagene",
"number_of_dns_query_24_hours": "Antall DNS-forespørsler som ble behandlet de siste 24 timene",
"number_of_dns_query_blocked_24_hours": "Antall DNS-forespørsler som ble blokkert av adblock-filtre, hosts-lister, og domene-lister",
"number_of_dns_query_blocked_24_hours_by_sec": "Antall DNS-forespørsler som ble blokkert av AdGuard sin nettlesersikkerhetsmodul",
"number_of_dns_query_blocked_24_hours_adult": "Antall voksennettsteder som ble blokkert",
"enforced_save_search": "Påtvungede barnevennlige søk",
"number_of_dns_query_to_safe_search": "Antall DNS-forespørsler til søkemotorer der \"Safe Search\" ble fremtvunget",
"average_processing_time": "Gjennomsnittlig behandlingstid",
"average_processing_time_hint": "Gjennomsnittstid for behandling av DNS-forespørsler i millisekunder",
"block_domain_use_filters_and_hosts": "Blokker domener ved hjelp av filtre, «hosts»-filer, og rå domener",
"filters_block_toggle_hint": "Du kan sette opp blokkeringsoppføringer i <a href='#filters'>Filtre</a>-innstillingene.",
"use_adguard_browsing_sec": "Benytt AdGuard sin nettlesersikkerhetstjeneste",
"use_adguard_browsing_sec_hint": "AdGuard Home vil sjekke om domenet har blitt svartelistet av nettlesersikkerhetstjenesten. Den vil bruke en privatlivsvennlig søke-API til å utføre sjekken: kun en kort prefiks av domenenavnet med SHA256-salting blir sendt til tjeneren.",
"use_adguard_parental": "Benytt AdGuard sin foreldrekontroll-nettjeneste",
"use_adguard_parental_hint": "AdGuard Home vil sjekke om domenet inneholder erotisk materiale. Den benytter den samme privatlivsvennlige API-en som nettlesersikkerhetstjenesten.",
"enforce_safe_search": "Påtving barnevennlige søk",
"enforce_save_search_hint": "AdGuard Home kan fremtvinge \"Safe Search\" i de følgende søkemotorene: Google, YouTube, Bing, DuckDuckGo, og Yandex.",
"no_servers_specified": "Ingen tjenere er spesifisert",
"general_settings": "Generelle innstillinger",
"dns_settings": "DNS-innstillinger",
"encryption_settings": "Krypteringsinnstillinger",
"dhcp_settings": "DHCP-innstillinger",
"upstream_dns": "Oppstrøms-DNS-tjenere",
"test_upstream_btn": "Test oppstrømstilkoblinger",
"apply_btn": "Benytt",
"disabled_filtering_toast": "Skrudde av filtrering",
"enabled_filtering_toast": "Skrudde på filtrering",
"disabled_safe_browsing_toast": "Skrudde av barnevennlig nettlesing",
"enabled_safe_browsing_toast": "Skrudde på barnevennlig nettlesing",
"disabled_parental_toast": "Skrudde av foreldrekontroll",
"enabled_parental_toast": "Skrudde på foreldrekontroll",
"disabled_safe_search_toast": "Skrudde av barnevennlige søk",
"enabled_save_search_toast": "Skrudde på barnevennlige søk",
"enabled_table_header": "Skrudd på",
"name_table_header": "Navn",
"filter_url_table_header": "Filterets URL",
"rules_count_table_header": "Antall oppføringer",
"last_time_updated_table_header": "Senest oppdatert",
"actions_table_header": "Handlinger",
"edit_table_action": "Rediger",
"delete_table_action": "Slett",
"filters_and_hosts": "Filtre og «hosts»-lister",
"filters_and_hosts_hint": "AdGuard Home forstår grunnleggende adblock-oppføringer, «hosts»-filsyntaks, og domenelister.",
"no_filters_added": "Ingen filtre er lagt til",
"add_filter_btn": "Legg til et filter",
"cancel_btn": "Avbryt",
"enter_name_hint": "Skriv inn navn",
"enter_url_hint": "Skriv inn nettadresse",
"check_updates_btn": "Se etter oppdateringer",
"new_filter_btn": "Nytt filterabonnement",
"enter_valid_filter_url": "Skriv inn en gyldig URL til et filterlisteabonnement.",
"custom_filter_rules": "Selvvalgte filtreringsregler",
"custom_filter_rules_hint": "Skriv inn én oppføring per linje. Du kan bruke adblock-oppføringer, «hosts»-filsyntaks, eller rå domener.",
"examples_title": "Eksempler",
"example_meaning_filter_block": "blokker tilgang til 'example.org'-domenet og alle dens underdomener",
"example_meaning_filter_whitelist": "opphev blokkeringen av 'example.org'-domenet og alle dens underdomener",
"example_meaning_host_block": "AdGuard Home vil nå videresende 'example.org'-domenet (men ikke dens underdomener) til 127.0.0.1.",
"example_comment": "! Her er det en kommentar",
"example_comment_meaning": "bare en kommentar",
"example_comment_hash": "# Også en kommentar",
"example_regex_meaning": "blokker tilgang til domener som samsvarer med den valgte ordinære oppføringen",
"example_upstream_regular": "vanlig DNS (over UDP)",
"example_upstream_dot": "kryptert <0>DNS-over-TLS</0>",
"example_upstream_doh": "kryptert <0>DNS-over-HTTPS</0>",
"example_upstream_sdns": "du kan bruke <0>DNS-stempler</0> med <1>DNSCrypt</1> eller <2>DNS-over-HTTPS</2>-behandlere",
"example_upstream_tcp": "vanlig DNS (over TCP)",
"all_filters_up_to_date_toast": "Alle filterlistene er allerede fullt oppdatert",
"updated_upstream_dns_toast": "Oppdaterte oppstrøms-DNS-tjenerne",
"dns_test_ok_toast": "De spesifiserte DNS-tjenerne fungerer riktig",
"dns_test_not_ok_toast": "Tjeneren «{{key}}» kunne ikke brukes, vennligst dobbeltsjekk at du har skrevet den riktig",
"unblock_btn": "Tillat",
"block_btn": "Blokker",
"time_table_header": "Tidspunkt",
"domain_name_table_header": "Domenenavn",
"type_table_header": "Type",
"response_table_header": "Respons",
"client_table_header": "Klient",
"empty_response_status": "Tomt innhold",
"show_all_filter_type": "Vis alle",
"show_filtered_type": "Vis kun filtrerte",
"no_logs_found": "Ingen loggføringer ble funnet",
"refresh_btn": "Oppfrisk",
"previous_btn": "Forrige",
"next_btn": "Neste",
"loading_table_status": "Laster inn …",
"page_table_footer_text": "Side",
"of_table_footer_text": "av",
"rows_table_footer_text": "rekker",
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene",
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrert av {{filter}}",
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",
"query_log_cleared": "Forespørselsloggen ble vellykket slettet",
"query_log_clear": "Tøm forespørselsloggene",
"query_log_retention": "Beholding av forespørselsloggføringene",
"query_log_enable": "Skru på loggføring",
"query_log_configuration": "Loggføringskonfigurasjon",
"query_log_disabled": "Forespørselsloggen er skrudd av og kan bli satt opp i <0>innstillingene</0>",
"query_log_strict_search": "Bruk anførselstegn for strenge søk",
"query_log_retention_confirm": "Er du sikker på at du vil endre hvor lenge forespørselsloggføringene skal beholdes? Hvis du reduserer den interne verdien, vil noe av dataene gå tapt",
"source_label": "Kilde",
"found_in_known_domain_db": "Funnet i databasen over kjente domener.",
"category_label": "Kategori",
"rule_label": "Oppføring",
"filter_label": "Filter",
"unknown_filter": "Ukjent filter {{filterId}}",
"install_welcome_title": "Velkommen til AdGuard Home!",
"install_welcome_desc": "AdGuard Home er en nettverksdekkende reklame-og-sporings-blokkerende DNS-tjener. Formålet dens er å la deg styre hele nettverket ditt og alle dine enheter, og den krever ikke at klientene bruker spesifikke programmer.",
"install_settings_title": "Admin-nettgrensesnitt",
"install_settings_listen": "Lytt til grensesnitt",
"install_settings_port": "Port",
"install_settings_interface_link": "Ditt AdGuard Home-admin-nettgrensesnitt vil være tilgjengelig på de følgende adressene:",
"form_error_port": "Skriv inn en gyldig portverdi",
"install_settings_dns": "DNS-tjener",
"install_settings_dns_desc": "Du vil måtte sette opp enhetene eller ruteren din(e) til å bruke DNS-tjeneren på disse adressene:",
"install_settings_all_interfaces": "Alle grensesnitt",
"install_auth_title": "Autentisering",
"install_auth_desc": "Det er høyst anbefalt å sette opp passordautentisering på ditt AdGuard Home-adminnettgrensesnitt. Selv om du velger å bare gjøre den tilgjengelig på ditt lokale nettverk, er det fortsatt viktig å beskytte den fra ubegrenset tilgang.",
"install_auth_username": "Brukernavn",
"install_auth_password": "Passord",
"install_auth_confirm": "Bekreft passord",
"install_auth_username_enter": "Skriv inn brukernavn",
"install_auth_password_enter": "Skriv inn passord",
"install_step": "Trinn",
"install_devices_title": "Sett opp enhetene dine",
"install_devices_desc": "For å begynne å bruke AdGuard Home, må du sette opp enhetene dine til å bruke den.",
"install_submit_title": "Gratulerer!",
"install_submit_desc": "Oppsettsprosedyren er ferdig, og du er klar til å begynne å bruke AdGuard Home.",
"install_devices_router": "Ruter",
"install_devices_router_desc": "Dette oppsettet vil automatisk dekke alle enhetene som er koblet til hjemmeruteren din, og du vil ikke måtte sette opp hver av dem manuelt.",
"install_devices_address": "AdGuard Home-DNS-tjeneren lytter til de følgende adressene",
"install_devices_router_list_1": "Åpne innstillingene til ruteren din. Vanligvis kan du få tilgang til den på nettleseren din gjennom en URL (f.eks. http://192.168.0.1/ eller http://192.168.1.1/). Du kan bli spurt om å skrive inn passordet ditt. Hvis du ikke husker det, kan du som oftest tilbakestille passordet ditt ved å trykke på knapp på selve ruteren. Noen rutere krever et spesifikt program, som i så fall er ment å allerede ha blitt installert på din PC/mobil.",
"install_devices_router_list_2": "Finn DHCP-/DNS-innstillingene. Se etter DNS-bokstavene ved siden av et felt som tillater to eller tre sett med sifre, som hver er delt opp i fire grupper på 1-3 sifre.",
"install_devices_router_list_3": "Skriv inn din AdGuard Home-tjeners adresser her.",
"install_devices_windows_list_1": "Åpne «Kontrollpanel» gjennom Start-menyen eller et Windows-søk.",
"install_devices_windows_list_2": "Gå til «Nettverk og internett»-kategorien, og så til «Nettverks- og delingssenter».",
"install_devices_windows_list_3": "På den venstre siden av skjermen, finn «Endre innstillinger for nettverkskort» og klikk på den.",
"install_devices_windows_list_4": "Velg din aktive tilkobling, høyreklikk på den, og velg «Egenskaper».",
"install_devices_windows_list_5": "Finn «Internet Protocol versjon 4 (TCP/IP)» i listen, velg den, og så klikk på «Egenskaper» igjen.",
"install_devices_windows_list_6": "Velg «Bruk følgende DNS-serveradresser» og så skriv inn din AdGuard Home-tjeners adresser.",
"install_devices_macos_list_1": "Klikk på Apple-ikonet og gå til Systeminnstillinger.",
"install_devices_macos_list_2": "Klikk på «Nettverk».",
"install_devices_macos_list_3": "Velg den første tilkoblingen i listen din, og klikk på «Avansert».",
"install_devices_macos_list_4": "Velg DNS-fanen og skriv inn din AdGuard Home-tjeners adresser der.",
"install_devices_android_list_1": "Fra Android-startskjermen, trykk på «Innstillinger».",
"install_devices_android_list_2": "Velg «Wi-Fi» i menyen. Skjermen som lister opp alle de tilgjengelige nettverkene vil bli vist (det er umulig å velge selvvalgte DNS-adresser for mobiltilkoblinger uten en DNS-endringsapp).",
"install_devices_android_list_3": "Langtrykk på nettverket du er koblet til, og så trykk «Endre nettverket».",
"install_devices_android_list_4": "På noen enheter, vil du måtte huke av boksen for Avansert for se flere innstillinger. For å justere dine Android-DNS-innstillinger, vil du måtte endre IP-innstillingene fra DHCP til Statisk.",
"install_devices_android_list_5": "Endre de forvalgte 'DNS 1' og 'DNS 2'-verdiene til din AdGuard Home-tjeners adresser.",
"install_devices_ios_list_1": "Fra startskjermen, trykk på «Innstillinger».",
"install_devices_ios_list_2": "Velg Wi-Fi i den venstre menyen (det er umulig å sette opp DNS for mobildata-nettverk).",
"install_devices_ios_list_3": "Trykk på navnet til det nettverket som er aktivt for øyeblikket.",
"install_devices_ios_list_4": "I DNS-feltet, skriv inn din AdGuard Home-tjeners adresser.",
"get_started": "Kom i gang",
"next": "Neste",
"open_dashboard": "Åpne kontrollsenteret",
"install_saved": "Lagringen var vellykket",
"encryption_title": "Kryptering",
"encryption_desc": "Krypteringsstøtte (HTTPS/TLS) for både DNS og admin-nettgrensesnittet",
"encryption_config_saved": "Krypteringsoppsettet ble lagret",
"encryption_server": "Tjenerens navn",
"encryption_server_enter": "Skriv inn domenenavnet ditt",
"encryption_server_desc": "For å kunne bruke HTTPS, må du skrive inn tjenernavnet som samsvarer med ditt SSL-sertifikat.",
"encryption_redirect": "Automatisk omdiriger til HTTPS",
"encryption_redirect_desc": "Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.",
"encryption_https": "HTTPS-port",
"encryption_https_desc": "Dersom HTTPS-porten er satt opp, vil AdGuard Home sitt admin-grensesnitt være tilgjengelig gjennom HTTPS, og vil også sørge for DNS-over-HTTPS på «/dns-query»-plasseringen.",
"encryption_dot": "'DNS-over-TLS'-port",
"encryption_dot_desc": "Dersom denne porten er satt opp, vil AdGuard Home kjøre en 'DNS-over-TLS'-tjener på denne porten.",
"encryption_certificates": "Sertifikater",
"encryption_certificates_desc": "For å bruke kryptering, må du skrive inn et gyldig SSL-sertifikatkjede for domenet ditt. Du kan få et gratis sertifikat hos <0>{{link}}</0>, eller kjøpe et fra en av de troverdige sertifikatsautoritetene.",
"encryption_certificates_input": "Kopier / lim inn dine PEM-kodede sertifikater her.",
"encryption_status": "Status",
"encryption_expire": "Utløper",
"encryption_key": "Privat nøkkel",
"encryption_key_input": "Kopier / lim inn ditt sertifikats PEM-kodede private nøkkel her.",
"encryption_enable": "Skru på kryptering (HTTPS, DNS-over-HTTPS, og DNS-over-TLS)",
"encryption_enable_desc": "Dersom kryptering er skrudd på, vil AdGuard Home sitt admingrensesnitt virke over HTTPS, og DNS-tjeneren vil lytte etter forespørseler over DNS-over-HTTPS og DNS-over-TLS.",
"encryption_chain_valid": "Sertifikatskjeden er gyldig",
"encryption_chain_invalid": "Sertifikatskjeden er ugyldig",
"encryption_key_valid": "Dette er en gyldig {{type}}-type privat nøkkel",
"encryption_key_invalid": "Dette er en ugyldig {{type}}-type privat nøkkel",
"encryption_subject": "Tema",
"encryption_issuer": "Utsteder",
"encryption_hostnames": "Vertsnavn",
"encryption_reset": "Er du sikker på at du vil tilbakestille krypteringsinnstillingene?",
"topline_expiring_certificate": "Ditt SSL-sertifikat er i ferd med å utløpe. Oppdater <0>Krypteringsinnstillinger</0>.",
"topline_expired_certificate": "SSL-sertifikatet har utløpt. Oppdater <0>Krypteringsinnstillinger</0>.",
"form_error_port_range": "Skriv inn et portnummer i området 80-65535",
"form_error_port_unsafe": "Denne porten er ikke trygg",
"form_error_equal": "Burde ikke være de samme",
"form_error_password": "Passordet samsvarer ikke",
"reset_settings": "Tilbakestill innstillinger",
"update_announcement": "AdGuard Home {{version}} er nå tilgjengelig! <0>Klikk her</0> for mere informasjon.",
"setup_guide": "Oppsettsveiledning",
"dns_addresses": "DNS-adresser",
"down": "Nedstrøm",
"fix": "Fiks",
"dns_providers": "Her er en <0>liste over kjente DNS-leverandører</0> som du kan velge blant.",
"update_now": "Oppdater nå",
"update_failed": "Auto-oppdatering mislyktes. Vennligst <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>følg trinnene</a> for manuelle oppdateringer.",
"processing_update": "Vennligst vent, AdGuard Home blir oppdatert",
"clients_title": "Klienter",
"clients_desc": "Konfigurer enheter som er koblet til AdGuard Home",
"settings_global": "Overbestyrt",
"settings_custom": "Tilpasset",
"table_client": "Klient",
"table_name": "Navn",
"save_btn": "Lagre",
"client_add": "Legg til klient",
"client_new": "Ny klient",
"client_edit": "Rediger klienten",
"client_identifier": "Identifikator",
"ip_address": "IP-adresse",
"form_enter_ip": "Skriv inn IP",
"form_enter_mac": "Skriv inn MAC",
"form_client_name": "Skriv inn klientnavnet",
"client_global_settings": "Bruk de overbestyrte innstillingene",
"client_deleted": "Klienten «{{key}}» ble vellykket slettet",
"client_added": "Klienten «{{key}}» ble vellykket lagt til",
"client_updated": "Klienten «{{key}}» ble vellykket oppdatert",
"clients_not_found": "Ingen klienter ble funnet",
"client_confirm_delete": "Er du sikker på at du vil slette klienten «{{key}}»?",
"filter_confirm_delete": "Er du sikker på at du vil slette filteret?",
"auto_clients_title": "Klienter (kjørende)",
"auto_clients_desc": "Loggføring over klientene som bruker AdGuard Home, men som ikke har blitt lagret i oppsettet",
"access_title": "Tilgangsinnstillinger",
"access_desc": "Her kan du sette opp tilgangsregler for AdGuard Home-DNS-tjeneren.",
"access_allowed_title": "Tillatte klienter",
"access_allowed_desc": "En liste over CIDR- eller IP-adresser. Dersom dette er satt opp, vil AdGuard Home kun akseptere forespørsler fra disse IP-adressene.",
"access_disallowed_title": "Klienter som skal avvises",
"access_disallowed_desc": "En liste over CIDR- eller IP-adresser. Dersom dette er satt opp, vil AdGuard Home avslå forespørsler fra disse IP-adressene.",
"access_blocked_title": "Blokkerte domener",
"access_blocked_desc": "Ikke forveksle dette med filtre. AdGuard Home vil nekte å behandle DNS-forespørsler som har disse domenene i forespørselene.",
"access_settings_saved": "Tilgangsinnstillingene ble vellykket lagret",
"updates_checked": "Oppdateringene ble vellykket sett etter",
"updates_version_equal": "AdGuard Home er fullt oppdatert",
"check_updates_now": "Se etter oppdateringer nå",
"dns_privacy": "DNS-privatliv",
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> Benytt <1>{{address}}</1>-strengen.",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> Benytt <1>{{address}}</1>-strengen.",
"setup_dns_privacy_3": "<0>Vennligst bemerk at krypterte DNS-protokoller bare er støttet på Android 9. Så du må installere ytterligere programvare for andre operativsystemer.</0><0>Her er en liste over programvare som du kan bruke.</0>",
"setup_dns_privacy_android_1": "Android 9 har innebygd støtte for DNS-over-TLS. For å sette det opp, gå til Innstillinger → Nettverk og internett → Avansert → Privat DNS, og skriv inn domenenavnet ditt der.",
"setup_dns_privacy_android_2": "<0>AdGuard for Android</0> støtter <1>DNS-over-HTTPS</1> og <1>DNS-over-TLS</1>.",
"setup_dns_privacy_android_3": "<0>Intra</0> legger til <1>DNS-over-HTTPS</1>-støtte i Android.",
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> støtter <1>DNS-over-HTTPS</1>, men for å sette det opp til å bruke din egen tjener, vil du måtte generere et <2>DNS-stempel</2> for det.",
"setup_dns_privacy_ios_2": "<0>AdGuard for iOS</0> støtter <1>DNS-over-HTTPS</1>- og <1>DNS-over-TLS</1>-oppsett.",
"setup_dns_privacy_other_title": "Andre implementeringer",
"setup_dns_privacy_other_1": "AdGuard Home i seg selv kan brukes som en sikker DNS-klient for enhver plattform.",
"setup_dns_privacy_other_2": "<0>dnsproxy</0> støtter alle kjente sikre DNS-protokoller.",
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> støtter <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> støtter <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Du finner flere implementeringer <0>her</0> og <1>her</1>.",
"setup_dns_notice": "For å benytte <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, må du <0>sette opp Kryptering</0> i AdGuard Home-innstillingene.",
"rewrite_added": "DNS-omdirigeringen for «{{key}}» ble vellykket lagt til",
"rewrite_deleted": "DNS-omdirigeringen for «{{key}}» ble vellykket slettet",
"rewrite_add": "Legg til DNS-omdirigering",
"rewrite_not_found": "Ingen DNS-omdirigeringer ble funnet",
"rewrite_confirm_delete": "Er du sikker på at du vil slette DNS-omdirigeringen for «{{key}}»?",
"rewrite_desc": "Lar deg enkelt konfigurere selvvalgte DNS-tilbakemeldinger for et spesifikt domenenavn.",
"rewrite_applied": "Benyttet omdirigeringsregelen",
"dns_rewrites": "DNS-omdirigeringer",
"form_domain": "Skriv inn domene",
"form_answer": "Skriv inn IP-adresse eller domenenavn",
"form_error_domain_format": "Ugyldig domeneformat",
"form_error_answer_format": "Ugyldig svarformat",
"configure": "Sett opp",
"main_settings": "Hovedinnstillinger",
"block_services": "Blokker spesifikke tjenester",
"blocked_services": "Blokkerte tjenester",
"blocked_services_desc": "Gjør det mulig å blokkere populære nettsteder og tjenester med letthet.",
"blocked_services_saved": "Tjenesteblokkeringene ble vellykket lagret",
"blocked_services_global": "Bruk de overbestyrt blokkerte tjenestene",
"blocked_service": "Blokkert tjeneste",
"block_all": "Blokker alt",
"unblock_all": "Tillat alt",
"encryption_certificate_path": "Filbanen til sertifikatet",
"encryption_private_key_path": "Filbanen til den private nøkkelen",
"encryption_certificates_source_path": "Bestem en filbane for sertifikater",
"encryption_certificates_source_content": "Lim inn innholdet til sertifikatet",
"encryption_key_source_path": "Bestem en privat nøkkelfil",
"encryption_key_source_content": "Lim inn innholdet til den private nøkkelen",
"stats_params": "Statistikk-oppsett",
"config_successfully_saved": "Oppsettet ble vellykket lagret",
"interval_24_hour": "24 timer",
"interval_days": "{{count}} dag",
"interval_days_plural": "{{count}} dager",
"domain": "Domene",
"answer": "Svar",
"filter_added_successfully": "Filteret har blitt vellykket lagt til",
"statistics_configuration": "Statistikk-oppsett",
"statistics_retention": "Statistikkbeholding",
"statistics_retention_desc": "Hvis du reduserer intervallverdien, vil noen av dataene gå tapt",
"statistics_clear": " Tøm statistikkene",
"statistics_clear_confirm": "Er du sikker på at du vil slette statistikkene?",
"statistics_retention_confirm": "Er du sikker på at du vil endre hvor lenge statistikkene skal beholdes? Hvis du reduserer den interne verdien, vil noe av dataene gå tapt",
"statistics_cleared": "Statistikkene ble vellykket tømt",
"interval_hours": "{{count}} time",
"interval_hours_plural": "{{count}} timer",
"filters_configuration": "Oppsett av filtre",
"filters_enable": "Skru på filtre",
"filters_interval": "Filteroppdateringsvanlighet",
"disabled": "Skrudd av",
"username_label": "Brukernavn",
"username_placeholder": "Skriv inn brukernacvn",
"password_label": "Passord",
"password_placeholder": "Skriv inn passord",
"sign_in": "Logg på",
"sign_out": "Logg av",
"forgot_password": "Har du glemt passordet?",
"forgot_password_desc": "Vennligst følg <0>disse trinnene</0> for å lage et nytt passord til brukerkontoen din.",
"location": "Posisjon",
"orgname": "Firmanavn",
"netname": "Nettverksnavn",
"descr": "Beskrivelse",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Lær mer</0> om å lage dine egne filterlister for AdGuard Home."
}

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "Dzierżawa DHCP",
"dhcp_static_leases": "Dzierżawy statyczne DHCP",
"dhcp_leases_not_found": "Nie znaleziono dzierżaw DHCP",
"dhcp_config_saved": "Zapisana konfiguracja serwera DHCP",
"form_error_required": "Pole wymagane",
"form_error_ip_format": "Nieprawidłowy format IPv4",
"form_error_mac_format": "Nieprawidłowy format MAC",
"form_error_positive": "Musi być większa niż 0",
"dhcp_form_gateway_input": "Adres IP bramy",
@@ -96,14 +94,12 @@
"use_adguard_parental": "Użyj usługi Kontrola Rodzicielska AdGuard",
"use_adguard_parental_hint": "AdGuard Home sprawdzi, czy domena zawiera materiały dla dorosłych. Używa tego samego interfejsu API przyjaznego prywatności, co usługa sieciowa Bezpieczne Przeglądanie. ",
"enforce_safe_search": "Wymuszaj bezpieczne wyszukiwanie",
"enforce_save_search_hint": "AdGuard Home może wymusić bezpieczne wyszukiwanie w następujących wyszukiwarkach: Google, Youtube, Bing i Yandex.",
"no_servers_specified": "Nie określono serwerów",
"general_settings": "Ustawienia główne",
"dns_settings": "Ustawienia DNS",
"encryption_settings": "Ustawienia szyfrowania",
"dhcp_settings": "Ustawienia DHCP",
"upstream_dns": "Serwery DNS z wyższego poziomu",
"upstream_dns_hint": "Jeśli pozostawisz te pole puste, AdGuard użyje <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> jako upstream. Użyj prefiksu tls:// dla serwera DNS over TLS.",
"test_upstream_btn": "Test upstreamów",
"apply_btn": "Zastosuj",
"disabled_filtering_toast": "Wyłączone filtrowanie",
@@ -141,7 +137,6 @@
"example_comment": "! Tutaj idzie komentarz",
"example_comment_meaning": "komentarz",
"example_comment_hash": "# Również komentarz",
"example_regex_meaning": "blokuj dostęp do domen pasujących do określonego wyrażenia regularnego",
"example_upstream_regular": "normalny DNS (przez UDP)",
"example_upstream_dot": "zaszyfrowany <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-przez-TLS</a>",
"example_upstream_doh": "zaszyfrowany <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-przez-HTTPS</a>",
@@ -287,7 +282,6 @@
"client_edit": "Edytuj klienta",
"client_identifier": "Identyfikator",
"ip_address": "Adres IP",
"client_identifier_desc": "Klientów można zidentyfikować po adresie IP lub adresie MAC. Należy pamiętać, że używanie MAC jako identyfikatora jest możliwe tylko wtedy, gdy AdGuard Home jest również <0>serwerem DHCP</0>",
"form_enter_ip": "Wpisz adres IP",
"form_enter_mac": "Wpisz adres MAC",
"form_client_name": "Wpisz nazwę klienta",

View File

@@ -17,9 +17,11 @@
"dhcp_leases": "Concessões DHCP",
"dhcp_static_leases": "Concessões de DHCP estático",
"dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
"dhcp_config_saved": "Salvar configurações do servidor DHCP",
"dhcp_config_saved": "Configurações DHCP salvas com sucesso",
"form_error_required": "Campo obrigatório",
"form_error_ip_format": "formato de endereço IPv4 inválido",
"form_error_ip4_format": "Formato de endereço IPv4 inválido",
"form_error_ip6_format": "Formato de endereço IPv6 inválido",
"form_error_ip_format": "Formato de endereço IPv inválido",
"form_error_mac_format": "Formato do endereço MAC inválido",
"form_error_positive": "Deve ser maior que 0",
"dhcp_form_gateway_input": "IP do gateway",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nova concessão estática",
"dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
"dhcp_add_static_lease": "Adicionar nova concessão estática",
"dhcp_reset": "Você tem certeza de que deseja redefinir a configuração DHCP?",
"delete_confirm": "Você tem certeza de que deseja excluir \"{{key}}\"?",
"form_enter_hostname": "Digite o hostname",
"error_details": "Detalhes do erro",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Usar o serviço de controle parental do AdGuard",
"use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Ele usa a mesma API amigável de privacidade que o serviço de segurança da navegação.",
"enforce_safe_search": "Forçar pesquisa segura",
"enforce_save_search_hint": "O AdGuard Home pode forçar a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing e Yandex.",
"enforce_save_search_hint": "O AdGuard Home pode forçar a pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Nenhum servidor especificado",
"general_settings": "Configurações gerais",
"dns_settings": "Configurações de DNS",
"encryption_settings": "Configurações de criptografia",
"dhcp_settings": "Configurações de DHCP",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Se você deixar este campo vazio, o AdGuard Home irá usar o<a href='https://1.1.1.1/' target='_blank'>DNS da Cloudflare</a> como upstream.",
"upstream_dns_hint": "Se você deixar este campo em branco, o AdGuard Home irá usar o <a href='https://www.quad9.net/' target='_blank'>Quad9</a> como upstream.",
"test_upstream_btn": "Testar upstreams",
"upstreams": "Upstreams",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtragem desativada",
"enabled_filtering_toast": "Filtragem ativada",
@@ -279,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} está disponível!<0>Clique aqui</0> para mais informações.",
"setup_guide": "Guia de configuração",
"dns_addresses": "Endereços DNS",
"dns_start": "O servidor DNS está iniciando",
"dns_status_error": "Ocorreu um erro ao obter o status do servidor DNS",
"down": "Caiu",
"fix": "Corrigido",
"dns_providers": "Aqui está uma <0>lista de provedores de DNS conhecidos</0> para escolher.",
@@ -297,9 +303,11 @@
"client_edit": "Editar cliente",
"client_identifier": "Identificador",
"ip_address": "Endereço de IP",
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
"client_identifier_desc": "Clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
"form_enter_ip": "Digite o endereço de IP",
"form_enter_mac": "Digite o endereço MAC",
"form_enter_id": "Inserir identificador",
"form_add_id": "Adicionar identificador",
"form_client_name": "Digite o nome do cliente",
"client_global_settings": "Usar configurações global",
"client_deleted": "Cliente \"{{key}}\" excluído com sucesso",
@@ -400,5 +408,6 @@
"netname": "Nome da rede",
"descr": "Descrição",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Saiba mais</0> sobre como criar as suas próprias listas negras de servidores."
"filtering_rules_learn_more": "<0>Saiba mais</0> sobre como criar as suas próprias listas negras de servidores.",
"blocked_by_response": "Bloqueado por CNAME ou IP na resposta"
}

View File

@@ -17,8 +17,10 @@
"dhcp_leases": "Concessões DHCP",
"dhcp_static_leases": "Concessões de DHCP estático",
"dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
"dhcp_config_saved": "Guardar configurações do servidor DHCP",
"dhcp_config_saved": "Configurações DHCP guardadas com sucesso",
"form_error_required": "Campo obrigatório",
"form_error_ip4_format": "Formato de endereço IPv4 inválido",
"form_error_ip6_format": "Formato de endereço IPv6 inválido",
"form_error_ip_format": "Formato de endereço IPv4 inválido",
"form_error_mac_format": "Formato do endereço MAC inválido",
"form_error_positive": "Deve ser maior que 0",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nova concessão estática",
"dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
"dhcp_add_static_lease": "Adicionar nova concessão estática",
"dhcp_reset": "Tem a certeza de que deseja redefinir a configuração DHCP?",
"delete_confirm": "Tem a certeza de que deseja excluir \"{{key}}\"?",
"form_enter_hostname": "Insira o hostname",
"error_details": "Detalhes do erro",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Usar o serviço de controlo parental do AdGuard",
"use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Usa a mesma API amigável de privacidade que o serviço de segurança da navegação.",
"enforce_safe_search": "Forçar pesquisa segura",
"enforce_save_search_hint": "O AdGuard Home pode forçar a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing e Yandex.",
"enforce_save_search_hint": "O AdGuard Home pode forçar a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Nenhum servidor especificado",
"general_settings": "Definições gerais",
"dns_settings": "Definições de DNS",
"encryption_settings": "Configurações de criptografia",
"dhcp_settings": "Configurações de DHCP",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Se deixar este campo vazio, o AdGuard Home irá usar o<a href='https://1.1.1.1/' target='_blank'>DNS da Cloudflare</a> como upstream.",
"upstream_dns_hint": "Se deixar este campo vazio, o AdGuard Home irá usar <a href='https://www.quad9.net/' target='_blank'>Quad9</a> como upstream.",
"test_upstream_btn": "Testar upstreams",
"upstreams": "Upstreams",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtragem desactivada",
"enabled_filtering_toast": "Filtragem activada",
@@ -279,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} está disponível!<0>Clique aqui</0> para mais informações.",
"setup_guide": "Guia de instalação",
"dns_addresses": "Endereços DNS",
"dns_start": "O servidor DNS está a iniciar",
"dns_status_error": "Erro ao obter o estado do servidor DNS",
"down": "Caiu",
"fix": "Corrigido",
"dns_providers": "Aqui está uma <0>lista de provedores de DNS conhecidos</0> para escolher.",
@@ -297,9 +303,11 @@
"client_edit": "Editar cliente",
"client_identifier": "Identificador",
"ip_address": "Endereço de IP",
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP, CIDR, ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
"form_enter_ip": "Insira IP",
"form_enter_mac": "Insira o endereço MAC",
"form_enter_id": "Inserir identificador",
"form_add_id": "Adicionar identificador",
"form_client_name": "Insira o nome do cliente",
"client_global_settings": "Usar configurações globais",
"client_deleted": "Cliente \"{{key}}\" excluído com sucesso",
@@ -400,5 +408,6 @@
"netname": "Nome da rede",
"descr": "Descrição",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Saiba mais</0>sobre como criar as suas próprias listas negras de servidores."
"filtering_rules_learn_more": "<0>Saiba mais</0>sobre como criar as suas próprias listas negras de servidores.",
"blocked_by_response": "Bloqueado por CNAME ou IP em resposta"
}

View File

@@ -17,11 +17,14 @@
"dhcp_leases": "Аренда DHCP",
"dhcp_static_leases": "Статические аренды DHCP",
"dhcp_leases_not_found": "Аренда DHCP не обнаружена",
"dhcp_config_saved": "Сохраненная конфигурация DHCP-сервера",
"dhcp_config_saved": "Конфигурация DHCP-сервера успешно сохранена",
"form_error_required": "Обязательное поле",
"form_error_ip_format": "Неверный формат IPv4",
"form_error_ip4_format": "Неверный формат IPv4",
"form_error_ip6_format": "Неверный формат IPv6",
"form_error_ip_format": "Неверный формат IP-адреса",
"form_error_mac_format": "Некорректный формат MAC",
"form_error_positive": "Должно быть больше 0",
"form_error_negative": "Должно быть не меньше 0",
"dhcp_form_gateway_input": "IP-адрес шлюза",
"dhcp_form_subnet_input": "Маска подсети",
"dhcp_form_range_title": "Диапазон IP-адресов",
@@ -43,6 +46,7 @@
"dhcp_new_static_lease": "Новая статическая аренда",
"dhcp_static_leases_not_found": "Не найдено статических аренд DHCP",
"dhcp_add_static_lease": "Добавить статическую аренду",
"dhcp_reset": "Вы уверены, что хотите сбросить настройки DHCP?",
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
"form_enter_hostname": "Введите имя хоста",
"error_details": "Детализация ошибки",
@@ -92,14 +96,14 @@
"use_adguard_parental": "Используйте модуль Родительского контроля AdGuard ",
"use_adguard_parental_hint": "AdGuard Home проверит, содержит ли домен материалы 18+. Он использует тот же API для обеспечения конфиденциальности, что и веб-служба безопасности браузера.",
"enforce_safe_search": "Усилить безопасный поиск",
"enforce_save_search_hint": "AdGuard Home может обеспечить безопасный поиск в следующих системах: Google, Youtube, Bing и Yandex.",
"enforce_save_search_hint": "AdGuard Home может обеспечить безопасный поиск в следующих поисковых системах: Google, Youtube, Bing, DuckDuckGo, Yandex и Pixabay.",
"no_servers_specified": "Нет указанных серверов",
"general_settings": "Основные настройки",
"dns_settings": "Настройки DNS",
"encryption_settings": "Настройки шифрования",
"dhcp_settings": "Настройки DHCP",
"upstream_dns": "Upstream DNS-серверы",
"upstream_dns_hint": "Если вы оставите это поле пустым, то AdGuard Home использует <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> в качестве upstream. Используйте tls:// для DNS через серверы TLS.",
"upstream_dns_hint": "Если вы оставите это поле пустым, то AdGuard Home использует <a href='https://www.quad9.net/' target='_blank'>Quad9</a> в качестве DNS сервера.",
"test_upstream_btn": "Тест upstream серверов",
"apply_btn": "Применить",
"disabled_filtering_toast": "Фильтрация выкл.",
@@ -179,6 +183,22 @@
"query_log_disabled": "Журнал запросов выключен, его можно включить в <0>настройках</0>",
"query_log_strict_search": "Используйте двойные кавычки для строгого поиска",
"query_log_retention_confirm": "Вы уверены, что хотите изменить срок хранения запросов? При сокращении интервала данные могут быть утеряны",
"dns_config": "Настройки DNS-сервера",
"blocking_mode": "Режим блокировки",
"nxdomain": "NXDOMAIN",
"null_ip": "Нулевой IP",
"custom_ip": "Свой IP",
"blocking_ipv4": "Блокировка IPv4",
"blocking_ipv6": "Блокировка IPv6",
"form_enter_rate_limit": "Введите rate limit",
"rate_limit": "Rate limit",
"edns_enable": "Включить отправку EDNS Client Subnet",
"edns_cs_desc": "Если включить эту опцию, AdGuard Home будет отправлять подсети клиентов на DNS-сервера.",
"rate_limit_desc": "Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно)",
"blocking_ipv4_desc": "IP-адрес, возвращаемый при блокировке A-запроса",
"blocking_ipv6_desc": "IP-адрес, возвращаемый при блокировке AAAA-запроса",
"blocking_mode_desc": "<0>«NXDOMAIN» — посылать в ответ код NXDOMAIN;</0><0>«Нулевой IP» — посылать в ответ нулевой IP-адрес (0.0.0.0 для A-запроса, :: для АААА-запроса);</0><0>«Свой IP» — посылать в ответ указанный IP-адрес.</0>",
"upstream_dns_client_desc": "Если оставить поле пустым, AdGuard Home будет обращаться к серверам, указанным в <0>настройках DNS</0>.",
"source_label": "Источник",
"found_in_known_domain_db": "Найден в базе известных доменов.",
"category_label": "Категория",
@@ -275,6 +295,8 @@
"update_announcement": "AdGuard Home {{version}} уже доступна! <0>Нажмите сюда</0>, чтобы узнать больше.",
"setup_guide": "Инструкция по настройке",
"dns_addresses": "Адреса DNS",
"dns_start": "DNS-сервер запускается",
"dns_status_error": "Ошибка при получении состояния DNS-сервера",
"down": "Вниз",
"fix": "Исправить",
"dns_providers": "<0>Список известных DNS-провайдеров</0> на выбор.",
@@ -293,9 +315,11 @@
"client_edit": "Редактировать клиента",
"client_identifier": "Идентификатор",
"ip_address": "IP-адрес",
"client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу или MAC-адресу. Обратите внимание, что использование MAC как идентификатора, возможно только если AdGuard Home также является и <0>DHCP-сервером</0>",
"client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу, CIDR или MAC-адресу. Обратите внимание, что использование MAC как идентификатора возможно, только если AdGuard Home также является и <0>DHCP-сервером</0>",
"form_enter_ip": "Введите IP",
"form_enter_mac": "Введите MAC",
"form_enter_id": "Введите идентификатор",
"form_add_id": "Добавить идентификатор",
"form_client_name": "Введите имя клиента",
"client_global_settings": "Использовать глобальные настройки",
"client_deleted": "Клиент \"{{key}}\" успешно удален",
@@ -404,5 +428,7 @@
"number_of_dns_query_days_0": "Количество DNS-запросов за {{count}} день",
"number_of_dns_query_days_1": "Количество DNS-запросов за {{count}} дня",
"number_of_dns_query_days_2": "Количество DNS-запросов за {{count}} дней",
"filtering_rules_learn_more": "<0>Узнайте больше</0> о создании собственных списков блокировки хостов."
"filtering_rules_learn_more": "<0>Узнайте больше</0> о создании собственных списков блокировки хостов.",
"blocked_by_response": "Заблокировано по CNAME или IP в ответе",
"try_again": "Попробовать еще раз"
}

View File

@@ -19,6 +19,8 @@
"dhcp_leases_not_found": "Neboli nájdené žiadne DHCP prenájmy",
"dhcp_config_saved": "Konfigurácia DHCP servera uložená",
"form_error_required": "Povinná položka",
"form_error_ip4_format": "Nesprávny formát IPv4",
"form_error_ip6_format": "Nesprávny formát IPv6",
"form_error_ip_format": "Nesprávny formát IPv4",
"form_error_mac_format": "Nesprávny MAC formát",
"form_error_positive": "Musí byť väčšie ako 0",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nový statický prenájom",
"dhcp_static_leases_not_found": "Nebol nájdený žiadny statický DHCP prenájom",
"dhcp_add_static_lease": "Pridať statický prenájom",
"dhcp_reset": "Naozaj chcete obnoviť konfiguráciu DHCP?",
"delete_confirm": "Naozaj chcete vymazať \"{{key}}\"?",
"form_enter_hostname": "Zadajte názov hostiteľa",
"error_details": "Podrobnosti chyby",
@@ -66,16 +69,22 @@
"disabled_protection": "Ochrana vypnutá",
"refresh_statics": "Obnoviť štatistiku",
"dns_query": "DNS dopyty",
"blocked_by": "<0>Blokované filtrami<0>",
"stats_malware_phishing": "Blokovaný škodlivý kód/pokus o podvod",
"stats_adult": "Blokovaná stránka pre dospelých",
"stats_query_domain": "Najčastejšie dopytované domény",
"for_last_24_hours": "za posledných 24 hodín",
"for_last_days": "za posledný {{count}} deň",
"for_last_days_plural": "za posledných {{count}} dní",
"no_domains_found": "Žiadna doména nebola nájdená",
"requests_count": "Počet dopytov",
"top_blocked_domains": "Najčastejšie zablokované domény",
"top_clients": "Najčastejší klienti",
"no_clients_found": "Neboli nájdení žiadni klienti",
"general_statistics": "Všeobecná štatistika",
"number_of_dns_query_days": "Počet DNS dopytov spracovaných za posledný {{count}} deň",
"number_of_dns_query_days_plural": "Počet DNS dopytov spracovaných za posledných {{count}} dní",
"number_of_dns_query_24_hours": "Počet DNS dopytov spracovaných za posledných 24 hodín",
"number_of_dns_query_blocked_24_hours": "Počet DNS dopytov zablokovaných filtrami reklamy a zoznamami adries",
"number_of_dns_query_blocked_24_hours_by_sec": "Počet DNS dopytov zablokovaných AdGuard modulom Bezpečné prehliadanie",
"number_of_dns_query_blocked_24_hours_adult": "Počet zablokovaných stránok pre dospelých",
@@ -97,8 +106,9 @@
"encryption_settings": "Nastavenia šifrovania",
"dhcp_settings": "Nastavenia DHCP",
"upstream_dns": "Upstream DNS servery",
"upstream_dns_hint": "Ak toto pole ponecháte prázdne, AdGuard Home použije <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> ako upstream.",
"upstream_dns_hint": "Ak toto pole ponecháte prázdne, AdGuard Home použije <a href='https://www.quad9.net/' target='_blank'>Quad9</a> ako upstream.",
"test_upstream_btn": "Test upstreamov",
"upstreams": "Upstreams",
"apply_btn": "Použiť",
"disabled_filtering_toast": "Vypnutá filtrácia",
"enabled_filtering_toast": "Zapnutá filtrácia",
@@ -135,6 +145,7 @@
"example_comment": "! Sem sa pridáva komentár",
"example_comment_meaning": "len komentár",
"example_comment_hash": "# Tiež komentár",
"example_regex_meaning": "zablokovať prístup k doménam, ktoré zodpovedajú zadanému regulárnemu výrazu",
"example_upstream_regular": "radová DNS (cez UDP)",
"example_upstream_dot": "šifrované <0>DNS-over-TLS</0>",
"example_upstream_doh": "šifrované <0>DNS-over-HTTPS</0>",
@@ -165,6 +176,17 @@
"updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá",
"rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel",
"rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel",
"query_log_response_status": "Stav: {{value}}",
"query_log_filtered": "Vyfiltrované pomocou {{filter}}",
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",
"query_log_cleared": "Denník dopytov bol úspešne vymazaný",
"query_log_clear": "Vymazať denníky dopytov",
"query_log_retention": "Obdobie záznamu denníka dopytov",
"query_log_enable": "Zapnúť denník",
"query_log_configuration": "Konfigurácia denníka",
"query_log_disabled": "Protokol dopytov je vypnutý a možno ho nakonfigurovať v <0>nastaveniach</0>",
"query_log_strict_search": "Na prísne vyhľadávanie použite dvojité úvodzovky",
"query_log_retention_confirm": "Naozaj chcete zmeniť uchovávanie denníku dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
"source_label": "Zdroj",
"found_in_known_domain_db": "Nájdené v databáze známych domén.",
"category_label": "Kategória",
@@ -182,6 +204,7 @@
"install_settings_dns_desc": "Budete musieť konfigurovať Vaše zariadenia alebo smerovač, aby používali DNS server na nasledujúcich adresách:",
"install_settings_all_interfaces": "Všetky rozhrania",
"install_auth_title": "Overenie identity",
"install_auth_desc": "Odporúčame Vám nakonfigurovať na administrátorskom webovom rozhraní AdGuard Home overenie Vašej identity heslom. Aj keď je prístupné iba vo Vašej lokálnej sieti, je stále dôležité chrániť ho pred neobmedzeným prístupom.",
"install_auth_username": "Meno používateľa",
"install_auth_password": "Heslo",
"install_auth_confirm": "Potvrdenie hesla",
@@ -260,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} je teraz k dispozícii! <0>Viac informácií nájdete tu</0>.",
"setup_guide": "Sprievodca nastavením",
"dns_addresses": "DNS adresy",
"dns_start": "Spúšťa sa DNS server",
"dns_status_error": "Chyba pri zisťovaní stavu DNS servera",
"down": "Nadol",
"fix": "Opraviť",
"dns_providers": "Tu je <0>zoznam známych poskytovateľov DNS</0>, z ktorého si vyberiete.",
@@ -281,6 +306,8 @@
"client_identifier_desc": "Klienti môžu byť identifikovaní podľa IP adresy alebo MAC adresy. Upozorňujeme, že používanie MAC ako identifikátora je možné len vtedy, ak je AdGuard Home tiež <0>DHCP server</0>",
"form_enter_ip": "Zadajte IP",
"form_enter_mac": "Zadajte MAC",
"form_enter_id": "Zadajte identifikátor",
"form_add_id": "Pridajte identifikátor",
"form_client_name": "Zadajte názov klienta",
"client_global_settings": "Použiť globálne nastavenia",
"client_deleted": "\"{{key}}\" klienta bol úspešne vymazaný",
@@ -318,5 +345,69 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Viac implementácií nájdete <0>tu</0> a <1>tu</1>.",
"setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>."
"setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>.",
"rewrite_added": "DNS prepísanie pre \"{{key}}\" bolo úspešne pridané",
"rewrite_deleted": "DNS prepísanie pre \"{{key}}\" bolo úspešne vymazané",
"rewrite_add": "Pridať DNS prepísanie",
"rewrite_not_found": "Neboli nájdené žiadne DNS prepísania",
"rewrite_confirm_delete": "Naozaj chcete odstrániť prepísanie DNS pre \"{{key}}\"?",
"rewrite_desc": "Umožňuje ľahko nakonfigurovať vlastnú odpoveď DNS pre konkrétny názov domény.",
"rewrite_applied": "Použilo sa pravidlo prepisovania",
"dns_rewrites": "DNS prepisovanie",
"form_domain": "Vložte meno domény",
"form_answer": "Zadajte IP adresu alebo názov domény",
"form_error_domain_format": "Neplatný formát domény",
"form_error_answer_format": "Neplatný formát odpovede",
"configure": "Konfigurovať",
"main_settings": "Hlavné nastavenia",
"block_services": "Blokovať vybrané služby",
"blocked_services": "Blokované služby",
"blocked_services_desc": "Umožňuje rýchlo blokovať populárne stránky a služby.",
"blocked_services_saved": "Blokované služby boli úspešne uložené",
"blocked_services_global": "Použite globálne blokované služby",
"blocked_service": "Blokované služby",
"block_all": "Blokovať všetko",
"unblock_all": "Odblokovať všetko",
"encryption_certificate_path": "Cesta k certifikátu",
"encryption_private_key_path": "Cesta k súkromného kľúču",
"encryption_certificates_source_path": "Nastavte cestu k súboru s certifikátom",
"encryption_certificates_source_content": "Vložiť obsah certifikátu",
"encryption_key_source_path": "Nastaviť súbor s privátnym kľúčom",
"encryption_key_source_content": "Vložte obsah privátneho kľúča",
"stats_params": "Konfigurácia štatistiky",
"config_successfully_saved": "Konfigurácia bola úspešne uložená",
"interval_24_hour": "24 hodín",
"interval_days": "{{count}} deň",
"interval_days_plural": "{{count}} dní",
"domain": "Doména",
"answer": "Odpoveď",
"filter_added_successfully": "Filter bol úspešne pridaný",
"statistics_configuration": "Konfigurácia štatistiky",
"statistics_retention": "Štatistika za obdobie",
"statistics_retention_desc": "Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
"statistics_clear": " Vynulovať štatistiku",
"statistics_clear_confirm": "Naozaj chcete vynulovať štatistiku?",
"statistics_retention_confirm": "Naozaj chcete zmeniť uchovávanie štatistík? Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
"statistics_cleared": "Štatistika bola úspešne vynulovaná",
"interval_hours": "{{count}} hodina",
"interval_hours_plural": "{{count}} hodín",
"filters_configuration": "Konfigurácia filtrov",
"filters_enable": "Zapnúť filtre",
"filters_interval": "Interval aktualizácie filtrov",
"disabled": "Vypnuté",
"username_label": "Meno používateľa",
"username_placeholder": "Zadajte meno používateľa",
"password_label": "Heslo",
"password_placeholder": "Zadajte heslo",
"sign_in": "Prihlásiť sa",
"sign_out": "Odhlásiť sa",
"forgot_password": "Zabudnuté heslo?",
"forgot_password_desc": "Postupujte podľa <0>týchto krokov</0> a vytvorte nové heslo pre svoj používateľský účet.",
"location": "Poloha",
"orgname": "Meno organizácie",
"netname": "Meno siete",
"descr": "Popis",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Viac informácií</0> o vytváraní vlastných zoznamov hostiteľov.",
"blocked_by_response": "Blokované pomocou CNAME alebo IP v odpovedi"
}

View File

@@ -19,7 +19,9 @@
"dhcp_leases_not_found": "Ni najdenih najemov DHCP",
"dhcp_config_saved": "Shranjena konfiguracija DHCP strežnika",
"form_error_required": "Zahtevano polje",
"form_error_ip_format": "Neveljaven format IPv4",
"form_error_ip4_format": "Neveljaven format IPv4",
"form_error_ip6_format": "Neveljaven format IPv6",
"form_error_ip_format": "Neveljaven format IP",
"form_error_mac_format": "Neveljaven MAC format",
"form_error_positive": "Mora biti večja od 0",
"dhcp_form_gateway_input": "IP prehoda",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "Nov statični najem",
"dhcp_static_leases_not_found": "Ni najdenih statičnih najemov DHCP",
"dhcp_add_static_lease": "Dodaj statičen najem",
"dhcp_reset": "Ali ste prepričani, da želite ponastaviti konfiguracijo DHCP?",
"delete_confirm": "Ali ste prepričani, da želite izbrisati \"{{key}}\"?",
"form_enter_hostname": "Vnesite ime gostitelja",
"error_details": "Podrobnosti o napaki",
@@ -96,15 +99,16 @@
"use_adguard_parental": "Uporabi AdGuardovo spletno storitev 'Starševski nadzor'",
"use_adguard_parental_hint": "AdGuard Home bo preveril, če domena vsebuje vsebine za odrasle. Uporablja enako, za zasebnost prijazen API, kot spletno storitev za varnost brskanja.",
"enforce_safe_search": "Vsili varno iskanje",
"enforce_save_search_hint": "AdGuard Home lahko uveljavi varno iskanje v naslednjih iskalnikih: Google, Youtube, Bing, DuckDuckGo in Yandex.",
"enforce_save_search_hint": "AdGuard Home lahko uveljavi varno iskanje v naslednjih iskalnikih: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Ni določenih strežnikov",
"general_settings": "Splošne nastavitve",
"dns_settings": "Nastavitve DNS",
"encryption_settings": "Nastavitve šifriranja",
"dhcp_settings": "Nastavitve DHCP",
"upstream_dns": "Zagonski DNS strežniki",
"upstream_dns_hint": "Če pustite to polje prazno, bo AdGuard Home uporabil <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> kot upstream.",
"upstream_dns_hint": "Če pustite to polje prazno, bo AdGuard Home uporabil <a href='https://www.quad9.net/' target='_blank'>Quad9</a> kot tok navzgor.",
"test_upstream_btn": "Preizkusi upstreame",
"upstreams": "Tokovi navzgor",
"apply_btn": "Uporabi",
"disabled_filtering_toast": "Onemogočeno filtriranje",
"enabled_filtering_toast": "Omogočeno filtriranje",
@@ -279,6 +283,8 @@
"update_announcement": "Zdaj je na voljo AdGuard Home {{version}}! <0>Klinite tukaj</0> za več informacij.",
"setup_guide": "Navodila za nastavitev",
"dns_addresses": "DNS naslovi",
"dns_start": "Zaganja se strežnik DNS",
"dns_status_error": "Napaka pri pridobivanju stanja strežnika DNS",
"down": "Navzdol",
"fix": "Popravi",
"dns_providers": "Tukaj je <0>seznam znanih ponudnikov DNS</0>, med katerimi lahko izbirate.",
@@ -297,9 +303,11 @@
"client_edit": "Uredi odjemalca",
"client_identifier": "Identifikator",
"ip_address": "IP naslov",
"client_identifier_desc": "Odjemalce lahko identificirate po naslovu IP ali naslovu MAC. Upoštevajte, da je uporaba MAC kot identifikatorja mogoča le, če je AdGuard Home tudi <0>DHCP strežnik</0>",
"client_identifier_desc": "Odjemalce je mogoče identificirati po naslovu IP, CIDR, MAC. Upoštevajte, da je uporaba MAC kot identifikatorja mogoča le, če je AdGuard Home tudi <0>DHCP strežnik</0>",
"form_enter_ip": "Vnesite IP",
"form_enter_mac": "Vnesite MAC",
"form_enter_id": "Vnesi identifikatorja",
"form_add_id": "Dodaj identifikatorja",
"form_client_name": "Vnesite ime odjemalca",
"client_global_settings": "Uporabi splošne nastavitve",
"client_deleted": "Odjemalec \"{{key}}\" je bil uspešno izbrisan",
@@ -400,5 +408,6 @@
"netname": "Ime omrežja",
"descr": "Opis",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Več o tem</0>, o ustvarjanju lastnih Seznamov nedovoljenih gostiteljev."
"filtering_rules_learn_more": "<0>Več o tem</0>, o ustvarjanju lastnih Seznamov nedovoljenih gostiteljev.",
"blocked_by_response": "Onemogočeno z CNAME ali IP v odgovoru"
}

View File

@@ -0,0 +1,431 @@
{
"client_settings": "Postavke klijenta",
"example_upstream_reserved": "možete odrediti DNS upstream <0>za određene domene</0>",
"upstream_parallel": "Koristite paralelne zahteve da ubrzate rešavanje istovremenim zahtevanjem svih servera",
"bootstrap_dns": "Bootstrap DNS serveri",
"bootstrap_dns_desc": "Bootstrap DNS serveri se koriste da reše IP adrese od DoH/DoT razrešivača koje ste odredili kao upstream.",
"check_dhcp_servers": "Proveri DHCP servere",
"save_config": "Sačuvaj konfiguraciju",
"enabled_dhcp": "DHCP server uključen",
"disabled_dhcp": "DHCP server isključen",
"dhcp_title": "DHCP server (eksperimentalno!)",
"dhcp_description": "Ako vaš ruter nema DHCP postavke, možete koristiti AdGuard' ugrađen DHCP server.",
"dhcp_enable": "Uključi DHCP server",
"dhcp_disable": "Isključi DHCP server",
"dhcp_not_found": "Bezbedno je da uključite ugrađeni DHCP server. Nismo pronašli nijedan aktivan DHCP server na mreži. međutim, ohrabrujemo vas da to ponovo proverite ručno, jer naš automatski test trenutno nije 100% pouzdan.",
"dhcp_found": "Pronađen je aktivan DHCP server na mreži. Nije bezbedno da uključite ugrađeni DHCP server.",
"dhcp_leases": "DHCP pozajmljivanja",
"dhcp_static_leases": "DHCP statička pozajmljivanja",
"dhcp_leases_not_found": "DHCP pozajmljivanja nisu pronađena",
"dhcp_config_saved": "Sačuvaj DHCP konfiguraciju servera",
"form_error_required": "Obavezno polje",
"form_error_ip4_format": "Nevažeći IPv4 format",
"form_error_ip6_format": "Nevažeći IPv6 format",
"form_error_ip_format": "Nevažeći IPv4 oblik",
"form_error_mac_format": "Nevažeći MAC format",
"form_error_positive": "Mora biti veće od 0",
"form_error_negative": "Mora biti 0 ili veće",
"dhcp_form_gateway_input": "IP mrežnog prolaza",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Opseg IP adresa",
"dhcp_form_range_start": "Početak opsega",
"dhcp_form_range_end": "Kraj opsega",
"dhcp_form_lease_title": "DHCP vreme pozajmljivanja (u sekundama)",
"dhcp_form_lease_input": "Trajanje iznajmljivanja",
"dhcp_interface_select": "Izaberite DHCP okruženje",
"dhcp_hardware_address": "Adresa hardvera",
"dhcp_ip_addresses": "IP adrese",
"dhcp_table_hostname": "Ime hosta",
"dhcp_table_expires": "Ističe",
"dhcp_warning": "Ako svakako želite da uključite DHCP server, uverite se da nema drugih aktivnih DHCP servera u vašoj mreži. U suprotnom, to može pokvariti Internet za povezane uređaje!",
"dhcp_error": "Ne možemo da odredimo da li ima drugih DHCP servera na mreži.",
"dhcp_static_ip_error": "Kako biste koristili DHCP server, morate podesiti statičku IP adresu. Nismo mogli da odredimo da li je ovo mrežno okruženje podešeno za korišćenje statičke IP adrese. Molimo vas da podesite statičku IP adresu ručno.",
"dhcp_dynamic_ip_found": "Vaš sistem koristi dinamičku IP adresu za okruženje <0>{{interfaceName}}</0>. Kako biste koristili DHCP server, morate podesiti statičku IP adresu. Vaša trenutna IP adresa je <0>{{ipAddress}}</0>. Automatski ćemo podesiti ovu IP adresu kao statičku ako pritisnete Uključi DHCP dugme.",
"dhcp_lease_added": "Statičko iznajmljivanje \"{{key}}\" uspešno dodato",
"dhcp_lease_deleted": "Statičko iznajmljivanje lease \"{{key}}\" uspešno izbrisano",
"dhcp_new_static_lease": "Novo statičko iznajmljivanje",
"dhcp_static_leases_not_found": "Nisu pronađena statička DHCP iznajmljivanja",
"dhcp_add_static_lease": "Dodaj statičko iznajmljivanje",
"dhcp_reset": "Jeste li sigurni da želite da resetujete DHCP konfiguraciju?",
"delete_confirm": "Jeste li sigurni da želite da izbrišete \"{{key}}\"?",
"form_enter_hostname": "Unesite ime hosta",
"error_details": "Detalji greške",
"back": "Nazad",
"dashboard": "Kontrolna tabla",
"settings": "Postavke",
"filters": "Filteri",
"query_log": "Dnevnik zahteva",
"faq": "ČPP",
"version": "Verzija",
"address": "adresa",
"on": "Uključeno",
"off": "Isključeno",
"copyright": "Autorska prava",
"homepage": "Početna stranica",
"report_an_issue": "Prijavi poteškoću",
"privacy_policy": "Politika privatnosti",
"enable_protection": "Uključi zaštitu",
"enabled_protection": "Uključena zaštita",
"disable_protection": "Isključi zaštitu",
"disabled_protection": "Isključena zaštita",
"refresh_statics": "Osveži statistike",
"dns_query": "DNS zahtevi",
"blocked_by": "<0>blokirano od filtera</0>",
"stats_malware_phishing": "Blokiraj štetan softver i fišing",
"stats_adult": "Blokiraj sajtove za odrasle",
"stats_query_domain": "Najčešće unošeni domeni",
"for_last_24_hours": "u poslednja 24 časa",
"for_last_days": "u poslednjih {{count}} dana",
"for_last_days_plural": "u poslednjih {{count}} dana",
"no_domains_found": "Domeni nisu pronađeni",
"requests_count": "Broj zahteva",
"top_blocked_domains": "Najčešće blokirani domeni",
"top_clients": "Najčešći klijenti",
"no_clients_found": "Nema pronađenih klijenata",
"general_statistics": "Opšte statistike",
"number_of_dns_query_days": "Broj obrađenih DNS unosa u poslednjih {{count}} dan",
"number_of_dns_query_days_plural": "Broj obrađenih DNS unosa u poslednjih {{count}} dana",
"number_of_dns_query_24_hours": "Broj obrađenih DNS unosa u poslednja 24 časa",
"number_of_dns_query_blocked_24_hours": "Broj DNS zahteva blokiranih od filtera blokatora reklama i blok liste hostova",
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahteva blokiranih od AdGuard-ovog podprograma za bezbedno pregledanje",
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih sajtova za odrasle",
"enforced_save_search": "Nametni sigurno pretraživanje",
"number_of_dns_query_to_safe_search": "Broj DNS zahteva ka pretraživačima za koje je nametnuto sigurno pretraživanje",
"average_processing_time": "Prosečno vreme obrade",
"average_processing_time_hint": "Prosečno vreme u milisekundama za obradu DNS zahteva",
"block_domain_use_filters_and_hosts": "Blokiraj domene koristeći filtere i hosts datoteke",
"filters_block_toggle_hint": "Možete postaviti pravila blokiranja u <a href='#filters'>Filters</a> postavkama.",
"use_adguard_browsing_sec": "Koristi AdGuard-ovu uslugu bezbednog pregledanja",
"use_adguard_browsing_sec_hint": "AdGuard Home će proveriti da li je domen blokiran od strane usluge za bezbednosno pregledanje. Koristiće prijateljski API privatni pregled da izvrši proveru. Samo će se kratak prefiks domena SHA256 hash poslati na server.",
"use_adguard_parental": "Koristi AdGuard-ovu uslugu roditeljske kontrole",
"use_adguard_parental_hint": "AdGuard Home će proveriti da li domen sadrži sadržaj za odrasle. Koristi se isti privatni prijateljski API kao i kod usluge bezbednog pregledanja.",
"enforce_safe_search": "Nametni sigurno pretraživanje",
"enforce_save_search_hint": "AdGuard Home može nametnuti sigurno pretraživanje u sledećim pretraživačima: Google, Youtube, Bing, DuckDuckGo i Yandex.",
"no_servers_specified": "Serveri nisu određeni",
"general_settings": "Opšte postavke",
"dns_settings": "DNS postavke",
"encryption_settings": "Postavke šifrovanja",
"dhcp_settings": "DHCP postavke",
"upstream_dns": "Upstream DNS serveri",
"upstream_dns_hint": "Ako ovo polje ostavite prazno, AdGuard Home će koristiti <a href='https://www.quad9.net/' target='_blank'>Quad9</a> kao upstream.",
"test_upstream_btn": "Testiraj upstreams",
"upstreams": "Upstreams",
"apply_btn": "Primeni",
"disabled_filtering_toast": "Isključeno filtriranje",
"enabled_filtering_toast": "Uključeno filtriranje",
"disabled_safe_browsing_toast": "Isključeno sigurno pregledanje",
"enabled_safe_browsing_toast": "Uključeno sigurno pretraživanje",
"disabled_parental_toast": "Isključena roditeljska kontrola",
"enabled_parental_toast": "Uključena roditeljska kontrola",
"disabled_safe_search_toast": "Isključena sigurna pretraga",
"enabled_save_search_toast": "Uključeno sigurno pretraživanje",
"enabled_table_header": "Uključeno",
"name_table_header": "Ime",
"filter_url_table_header": "URL do filtera",
"rules_count_table_header": "Broj pravila",
"last_time_updated_table_header": "Poslednji put ažurirano",
"actions_table_header": "Radnje",
"edit_table_action": "Izmeni",
"delete_table_action": "Izbriši",
"filters_and_hosts": "Blok lista filtera i hostova",
"filters_and_hosts_hint": "AdGuard Home razume osnovna pravila blokiranja reklama i sintaksu hosts datoteke.",
"no_filters_added": "Filteri nisu dodati",
"add_filter_btn": "Dodaj filter",
"cancel_btn": "Otkaži",
"enter_name_hint": "Unesite ime",
"enter_url_hint": "Unesite URL",
"check_updates_btn": "Proveri ažuriranja",
"new_filter_btn": "Nova pretplata na filter",
"enter_valid_filter_url": "Unesite važeći URL do pretplate na filter ili važeću hosts datoteku.",
"custom_filter_rules": "Prilagođena pravila filtriranja",
"custom_filter_rules_hint": "Unesite jedno pravilo po redu. Možete koristiti pravila blokatora reklama ili sintaksu hosts datoteke.",
"examples_title": "Primeri",
"example_meaning_filter_block": "blokirajte pristup ka domenu primer.org i svim njegovim poddomenima",
"example_meaning_filter_whitelist": "dozvolite pristup ka domenu primer.org i svim njegovim poddomenima",
"example_meaning_host_block": "AdGuard Home će sada vratiti adresu 127.0.0.1 za domen primer.org (ali ne i za njegove poddomene).",
"example_comment": "! Ovde ide komentar",
"example_comment_meaning": "samo komentar",
"example_comment_hash": "# Takođe komentar",
"example_regex_meaning": "blokiranje pristupa domenima koji odgovaraju određenom uobičajenom izrazu",
"example_upstream_regular": "uobičajeno DNS (preko UDP)",
"example_upstream_dot": "šifrovano <0>DNS-over-TLS</0>",
"example_upstream_doh": "šifrovano <0>DNS-over-HTTPS</0>",
"example_upstream_sdns": "možete koristiti <0>DNS brojeve</0> za <1>DNSCrypt</1> ili <2>DNS-over-HTTPS</2>",
"example_upstream_tcp": "uobičajeni DNS (preko TCP)",
"all_filters_up_to_date_toast": "Svi filteri su već ažurirani",
"updated_upstream_dns_toast": "Ažurirani upstream DNS serveri",
"dns_test_ok_toast": "Dati DNS serveri rade ispravno",
"dns_test_not_ok_toast": "Server \"{{key}}\": se ne može koristiti. Proverite da li ste ga ispravno uneli",
"unblock_btn": "Odblokiraj",
"block_btn": "Blokiraj",
"time_table_header": "Vreme",
"domain_name_table_header": "Ime domena",
"type_table_header": "Vrsta",
"response_table_header": "Odgovor",
"client_table_header": "Klijent",
"empty_response_status": "Prazno",
"show_all_filter_type": "Pokaži sve",
"show_filtered_type": "Pokaži filtrirano",
"no_logs_found": "Dnevnici nisu pronađeni",
"refresh_btn": "Osveži",
"previous_btn": "Prethodno",
"next_btn": "Sledeće",
"loading_table_status": "Učitavanje...",
"page_table_footer_text": "Stranica",
"of_table_footer_text": "od",
"rows_table_footer_text": "redovi",
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja",
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja",
"query_log_response_status": "Stanje: {{value}}",
"query_log_filtered": "Filtrirano od {{filter}}",
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",
"query_log_cleared": "Dnevnik unosa je uspešno očišćen",
"query_log_clear": "Očisti dnevnike unosa",
"query_log_retention": "Zadržavanje dnevnika unosa",
"query_log_enable": "Uključi dnevnik",
"query_log_configuration": "Konfiguracija dnevnika",
"query_log_disabled": "Dnevnik unosa je isključen ali se može konfigurisati u <0>postavkama</0>",
"query_log_strict_search": "Koristi duple navodnike za striktnu pretragu",
"query_log_retention_confirm": "Jeste li sigurni da želite da promenite zadržavanje dnevnika unosa? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
"dns_config": "Konfiguracija DNS servera",
"blocking_mode": "Način blokiranja",
"nxdomain": "NXDOMAIN",
"null_ip": "Null IP",
"custom_ip": "Prilagođeni IP",
"blocking_ipv4": "Blokiranje IPv4",
"blocking_ipv6": "Blokiranje IPv6",
"form_enter_rate_limit": "Unesite ograničenje brzine",
"rate_limit": "Ograničenje brzine",
"edns_enable": "Uključi EDNS Client Subnet",
"edns_cs_desc": "Ako je uključeno, AdGuard Home će slati klijente na DNS servere.",
"rate_limit_desc": "Broj zahteva po sekundi koje pojedinačni klijent dozvoljava (0: neograničeno)",
"blocking_ipv4_desc": "IP adresa koja će biti vraćena za blokirane zahteve",
"blocking_ipv6_desc": "IP adresa koja će biti vraćena za blokirane AAAA zahteve",
"blocking_mode_desc": "<0>NXDOMAIN odgovara sa NXDOMAIN code;</0> <0>Null IP odgovara sa nulom IP adresom (0.0.0.0 za A; :: za AAAA);</0> <0>Prilagođeni IP - odgovara sa ručno podešenom IP adresom",
"upstream_dns_client_desc": "AKo ovo polje ostavite prazno, AdGuard Home će koristiti servere konfigurisane u <0>DNS postavkama</0>.",
"source_label": "Izvor",
"found_in_known_domain_db": "Pronađeno u poznatim bazama podataka domena.",
"category_label": "Kategorija",
"rule_label": "Pravilo",
"filter_label": "Filter",
"unknown_filter": "Nepoznat filter {{filterId}}",
"install_welcome_title": "Dobrodošli u AdGuard home!",
"install_welcome_desc": "AdGuard Home je mrežni DNS server, blokator reklama i praćenja. Dopušta vam da kontrolišete svoju čitavu mrežu i sve vaše uređaje i ne zahteva korišćenje nikakvog klijentskog programa.",
"install_settings_title": "Administratorsko web okruženje",
"install_settings_listen": "Okruženje slušanja",
"install_settings_port": "Port",
"install_settings_interface_link": "Vaše AdGuard Home administratorsko web okruženje će biti dostupno na sledećim adresama:",
"form_error_port": "Unesite važeću vrednost za port",
"install_settings_dns": "DNS server",
"install_settings_dns_desc": "Potrebno je da konfigurišete vaše uređaje ili ruter da koristi DNS server sa sledećim adresama:",
"install_settings_all_interfaces": "Sva okruženja",
"install_auth_title": "Autentifikacija",
"install_auth_desc": "Preporučujemo vam da konfigurišete autentifikaciju lozinkom za vaše AdGuard Home administratorsko okruženje. Čak iako mu je moguće pristupiti samo iz vaše lokalne mreže,, i dalje je važno da ga zaštitite od neograničenog pristupa.",
"install_auth_username": "Korisničko ime",
"install_auth_password": "Lozinka",
"install_auth_confirm": "Potvrdite lozinku",
"install_auth_username_enter": "Unesite korisničko ime",
"install_auth_password_enter": "Unesite lozinku",
"install_step": "Korak",
"install_devices_title": "Konfigurišite vaše uređaje",
"install_devices_desc": "Za početak korišćenja AdGuard Home, potrebno je da konfigurišete vaše uređaje da ga koriste.",
"install_submit_title": "Čestitamo!",
"install_submit_desc": "Postavljanje je završeno i sada ste spremni da započnete sa korišćenjem AdGuard Home.",
"install_devices_router": "Ruter",
"install_devices_router_desc": "Ovo postavljanje će automatski pokriti sve uređaje koji su povezani na vaš kućni ruter pa nećete morati da konfigurišete svaki uređaj posebno.",
"install_devices_address": "AdGuard Home DNS server sluša na sledećim adresama",
"install_devices_router_list_1": "Otvorite podešavanja vašeg rutera. Obično im možete pristupiti iz vašeg preglednika preko URL (kao http://192.168.0.1/ ili http://192.168.1.1/). Možda će vam biti zatraženo da unesete lozinku. Ako je ne znate ili je se ne sećate, najčešće je možete resetovati pritiskom na dugme na samom ruteru. Neki ruteri zahtevaju određenu aplikaciju, koja bi u tom slučaju već trebalo da bude instalirana na vašem računaru ili telefonu.",
"install_devices_router_list_2": "Pronađite DHCP ili DNS postavke. Potražite DNS slova pored polja koje dozvoljava dve ili tri skupine brojeva, a svaka može da sadrži četiri grupe od jedne do tri cifre.",
"install_devices_router_list_3": "Tamo unesite adrese AdGuard home servera.",
"install_devices_windows_list_1": "Otvorite kontrolnu tablu iz startnog menija ili kroz Windows pretragu.",
"install_devices_windows_list_2": "Otvorite kategoriju mreža i internet a onda otiđite u centar za mrežu i deljenje.",
"install_devices_windows_list_3": "Na levoj strani ekrana pronađite Promena postavke adaptera i kliknite tu.",
"install_devices_windows_list_4": "Izaberite vašu aktivnu vezu, desnim tasterom kliknite na nju i izaberite Svojstva.",
"install_devices_windows_list_5": "Na listi pronađite Internet Protokol verzija 4 (TCP/IP), izaberite ga pa kliknite ponovo na Svojstva.",
"install_devices_windows_list_6": "Izaberite Koristi sledeće adrese DNS servera pa unesite vaše adrese AdGuard Home servera.",
"install_devices_macos_list_1": "Kliknite na ikonicu jabuke pa otiđite na postavke sistema.",
"install_devices_macos_list_2": "Kliknite na mrežu.",
"install_devices_macos_list_3": "Izaberite prvu vezu sa liste pa kliknite na više opcija.",
"install_devices_macos_list_4": "Izaberite karticu DNS pa tu unesite adrese vašeg AdGuard Home servera.",
"install_devices_android_list_1": "Sa Android početnog ekrana, dodirnite Postavke.",
"install_devices_android_list_2": "Dodirnite Wi-Fi. Pojaviće se ekran sa svim dostupnim mrežama. Nije moguće da podesite prilagođeni DNS za mobilne veze).",
"install_devices_android_list_3": "Dugo pritisnite na mrežu na koju ste povezani, pa dodirnite Izmeni mrežu.",
"install_devices_android_list_4": "Na nekim uređajima će možda biti potrebno da označite kućicu za napredne opcije kako bi videli dalje postavke. Da biste prilagodili vaše Android DNS postavke, prebacite IP postavke sa DHCP na statičke.",
"install_devices_android_list_5": "Promenite DNS 1 i DNS 2 vrednosti na adrese vašeg AdGuard Home servera.",
"install_devices_ios_list_1": "Sa početnog ekrana, dodirnite postavke.",
"install_devices_ios_list_2": "U levom meniju izaberite Wi-Fi. Nije moguće da konfigurišete DNS za mobilne mreže).",
"install_devices_ios_list_3": "Dodirnite ime trenutno aktivne mreže.",
"install_devices_ios_list_4": "U DNS polje unesite adrese vašeg AdGuard Home servera.",
"get_started": "Počnimo",
"next": "Dalje",
"open_dashboard": "Otvori kontrolnu tablu",
"install_saved": "Uspešno sačuvano",
"encryption_title": "Šifrovanje",
"encryption_desc": "Šifrovanje (HTTPS/TLS) podrška za oba DNS i administratorsko okruženje",
"encryption_config_saved": "Konfiguracija šifrovanja je sačuvana",
"encryption_server": "Ime servera",
"encryption_server_enter": "Unesite vaše ime domena",
"encryption_server_desc": "Kako biste koristili HTTPS, potrebno je da unesete ime servera koje se podudara sa SSL sertifikatom.",
"encryption_redirect": "Automatski preusmeri na HTTPS",
"encryption_redirect_desc": "Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "Ako je HTTPS port konfigurisan, AdGuard Home administratorskom okruženju će se moći pristupati preko HTTPS, a to će takođe omogućiti DNS-over-HTTPS na '/dns-query' lokaciji.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "Ako je ovaj port konfigurisan, AdGuard Home će pokretati DNS-over-TLS server na ovom portu.",
"encryption_certificates": "Sertifikati",
"encryption_certificates_desc": "Da biste koristili šifrovanje, morate obezbediti važeći lanac SSL sertifikata za vaš domen. Besplatan sertifikat možete nabaviti na <0>{{link}}</0> ili ga možete kupiti od nekog od pouzdanih izdavalaca sertifikata.",
"encryption_certificates_input": "Kopirajte/nalepite vaše PEM-kodirane sertifikate ovde.",
"encryption_status": "Stanje",
"encryption_expire": "Ističe",
"encryption_key": "Privatni ključ",
"encryption_key_input": "Kopirajte/nalepite vaš PEM-kodirani privatni ključ za vaš sertifikat ovde.",
"encryption_enable": "Uključi šifrovanje (HTTPS, DNS-over-HTTPS, i DNS-over-TLS)",
"encryption_enable_desc": "Ako je šifrovanje uključeno, AdGuard Home administratorsko okruženje će raditi preko HTTPS, i DNS server će slušati zahteve preko DNS-over-HTTPS i DNS-over-TLS.",
"encryption_chain_valid": "Lanac sertifikata je važeći",
"encryption_chain_invalid": "Lanac sertifikata je nevažeći",
"encryption_key_valid": "Ovo je važeći {{type}} privatni ključ",
"encryption_key_invalid": "Ovo je nevažeći {{type}} privatni ključ",
"encryption_subject": "Predmet",
"encryption_issuer": "Izdavač",
"encryption_hostnames": "Imena hostova",
"encryption_reset": "Jeste li sigurni da želite dda resetujete postavke šifrovanja?",
"topline_expiring_certificate": "Vaš SSL sertifikat uskoro ističe. Ažurirajte <0>postavke šifrovanja</0>.",
"topline_expired_certificate": "Vaš SSL sertifikat je istekao. Ažurirajte <0>postavke šifrovanja</0>.",
"form_error_port_range": "Unesite vrednost porta u opsegu od 80-65535",
"form_error_port_unsafe": "Ovo nije siguran port",
"form_error_equal": "Ne bi trebalo da bude jednako",
"form_error_password": "Lozinke se ne podudaraju",
"reset_settings": "Vrati postavke na podrazumevano",
"update_announcement": "AdGuard Home {{version}} je sada dostupan! <0>Kliknite ovde</0> za više informacija.",
"setup_guide": "Uputstvo za podešavanje",
"dns_addresses": "DNS adrese",
"dns_start": "DNS server se pokreće",
"dns_status_error": "Greška prilikom pribavljanja stanja DNS servera",
"down": "Dole",
"fix": "Popravi",
"dns_providers": "Ovo je a <0>lista poznatih DNS dobavljača</0> sa koje možete da izaberete.",
"update_now": "Ažuriraj sada",
"update_failed": "Automatsko ažuriranje nije uspelo. Molimo vas <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>pratite korake</a> za ručno ažuriranje.",
"processing_update": "Molimo sačekajte. AdGuard Home se ažurira",
"clients_title": "Klijenti",
"clients_desc": "Konfigurišite uređaje koji su povezani na AdGuard Home",
"settings_global": "Globalno",
"settings_custom": "Prilagođeno",
"table_client": "Klijent",
"table_name": "Ime",
"save_btn": "Sačuvaj",
"client_add": "Dodaj klijent",
"client_new": "Novi klijent",
"client_edit": "Izmeni klijent",
"client_identifier": "Identifikator",
"ip_address": "IP adresa",
"client_identifier_desc": "Klijenti mogu da budu prepoznati po IP adresi ili MAC adresi. Imajte na umu da je korišćenje MAC adrese kao identifikatora moguće samo ako je AdGuard Home takođe a <0>DHCP server</0>",
"form_enter_ip": "Unesite IP",
"form_enter_mac": "Unesite MAC",
"form_enter_id": "Unesite identifikator",
"form_add_id": "Dodaj identifikator",
"form_client_name": "Unesite ime klijenta",
"client_global_settings": "Koristi globalne postavke",
"client_deleted": "Klijent \"{{key}}\" uspešno izbrisan",
"client_added": "Klijent \"{{key}}\" uspešno dodat",
"client_updated": "Klijent \"{{key}}\" uspešno ažuriran",
"clients_not_found": "Nema pronađenih klijenata",
"client_confirm_delete": "Jeste li sigurni da želite da izbrišete klijenta \"{{key}}\"?",
"filter_confirm_delete": "Jeste li sigurni da želite da izbrišete filter?",
"auto_clients_title": "Klijenti (runtime)",
"auto_clients_desc": "Podaci o klijentima koji koriste AdGuard Home, ali nisu sačuvani u konfiguraciji",
"access_title": "Postavke pristupa",
"access_desc": "Ovde možete konfigurisati pravila pristupa za AdGuard home DNS server.",
"access_allowed_title": "Dozvoljeni klijenti",
"access_allowed_desc": "Spisak CIDR ili IP adresa. Ako je podešeno, AdGuard Home će prihvatiti zahteve samo od ovih IP adresa.",
"access_disallowed_title": "Zabranjeni klijenti",
"access_disallowed_desc": "Lista CIDR ili IP adresa.. Ako je podešeno, AdGuard Home će odbijati zahteve od ovih IP adresa.",
"access_blocked_title": "Blokirani domeni",
"access_blocked_desc": "Nemojte ovo mešati sa filterima. AdGuard Home će odbijati DNS unose ka ovim domenima.",
"access_settings_saved": "Postavke pristupa su uspešno sačuvane",
"updates_checked": "Ažuriranja su uspešno proverena",
"updates_version_equal": "AdGuard Home je ažuriran na najnoviju verziju",
"check_updates_now": "Proveri da li postoje ispravke",
"dns_privacy": "DNS privatnost",
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> koristi <1>{{address}}</1> string.",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> koristi <1>{{address}}</1> string.",
"setup_dns_privacy_3": "<0>Imajte na umu da su protokoli šifrovanog DNS-a podržani samo od Androida 9. Biće potrebno da instalirate dodatni softver za druge operativne sisteme.</0><0>Ovde je spisak softvera koje možete koristiti.</0>",
"setup_dns_privacy_android_1": "Android 9 podržava DNS-over-TLS. Za konfiguraciju, idite u postavke → mreža i internet → Napredno → Privatni DNS i tamo unesite ime vašeg domena.",
"setup_dns_privacy_android_2": "<0>AdGuard for Android</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.",
"setup_dns_privacy_android_3": "<0>Intra</0> dodaje <1>DNS-over-HTTPS</1> podršku za Android.",
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> podržava <1>DNS-over-HTTPS</1>, ali da biste mogli da ga konfigurišete da koristi vaš lični server, biće potrebno da generišete a <2>DNS Stamp</2> za njega.",
"setup_dns_privacy_ios_2": "<0>AdGuard za iOS</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.",
"setup_dns_privacy_other_title": "Druge implementacije",
"setup_dns_privacy_other_1": "AdGuard home može biti bezbedan DNS server na bilo kojoj platformi.",
"setup_dns_privacy_other_2": "<0>dnsproxy</0> podržava sve poznate bezbedne DNS protokole.",
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podržava <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Više implementacija ćete pronaći <0>ovde</0> i <1>ovde</1>.",
"setup_dns_notice": "Kako biste koristili <1>DNS-over-HTTPS</1> ili <1>DNS-over-TLS</1>, potrebno je da <0>konfigurišete šifrovanje</0> u AdGuard Home postavkama.",
"rewrite_added": "DNS prepisivanje za \"{{key}}\" je uspešno dodato",
"rewrite_deleted": "DNS prepisivanje za \"{{key}}\" uspešno izbrisano",
"rewrite_add": "Dodaj DNS prepisivanje",
"rewrite_not_found": "DNS prepisivanja nisu pronađena",
"rewrite_confirm_delete": "Jeste li sigurni da želite da izbrišete DNS prepisivanje za \"{{key}}\"?",
"rewrite_desc": "Dozvoljava da jednostavno konfigurišete prilagođeni DNS odgovor za određeni domen.",
"rewrite_applied": "Primenjeno pravilo prepisivanja",
"dns_rewrites": "DNS prepisivanja",
"form_domain": "Unesite domen",
"form_answer": "Unesite IP adresu ili domen",
"form_error_domain_format": "Nevažeći format domena",
"form_error_answer_format": "Nevažeći format odgovora",
"configure": "Konfiguriši",
"main_settings": "Glavne postavke",
"block_services": "Blokiraj određene usluge",
"blocked_services": "Blokiraj usluge",
"blocked_services_desc": "Dozvoljava vam da brzo blokirate popularne sajtove i usluge.",
"blocked_services_saved": "Blokirane usluge su uspešno sačuvane",
"blocked_services_global": "Koristi globalne blokirane usluge",
"blocked_service": "Blokirana usluga",
"block_all": "Blokiraj sve",
"unblock_all": "Odblokiraj sve",
"encryption_certificate_path": "Putanja sertifikata",
"encryption_private_key_path": "Putanja privatnog ključa",
"encryption_certificates_source_path": "Postavi putanju do datoteke sertifikata",
"encryption_certificates_source_content": "Nalepite sadržaj sertifikata",
"encryption_key_source_path": "Podesi datoteku privatnog ključa",
"encryption_key_source_content": "Nalepi sadržaj privatnog ključa",
"stats_params": "Konfiguracija statistike",
"config_successfully_saved": "Konfiguracija je uspešno sačuvana",
"interval_24_hour": "24 časa",
"interval_days": "{{count}} dan",
"interval_days_plural": "{{count}} dana",
"domain": "Domen",
"answer": "Odgovor",
"filter_added_successfully": "Filter je uspešno dodat",
"statistics_configuration": "Konfiguracija statistike",
"statistics_retention": "Zadržavanje statistike",
"statistics_retention_desc": "Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
"statistics_clear": " Očisti statistiku",
"statistics_clear_confirm": "Jeste li sigurni da želite da očistite statistiku?",
"statistics_retention_confirm": "Jeste li sigurni da želite da promenite zadržavanje statistike? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
"statistics_cleared": "Statistika je uspešno očišćena",
"interval_hours": "{{count}} čas",
"interval_hours_plural": "{{count}} časova",
"filters_configuration": "Konfiguracija filtera",
"filters_enable": "Uključi filtere",
"filters_interval": "Interval ažuriranja filtera",
"disabled": "Isključeno",
"username_label": "Korisničko ime",
"username_placeholder": "Unesite korisničko ime",
"password_label": "Lozinka",
"password_placeholder": "Unesite lozinku",
"sign_in": "Prijavi se",
"sign_out": "Odjavi se",
"forgot_password": "Zaboravili ste lozinku?",
"forgot_password_desc": "Ispratite <0>ove korake</0> za stvaranje nove lozinke za vaš korisnički nalog.",
"location": "Lokacija",
"orgname": "Ime organizacije",
"netname": "Ime mreže",
"descr": "Opis",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Saznajte više</0> o stvaranju vaše lične blokliste hostova.",
"blocked_by_response": "Blokirano od CNAME ili IP u odgovoru",
"try_again": "Pokušaj ponovo"
}

View File

@@ -15,10 +15,10 @@
"dhcp_not_found": "Ingen aktiv DHCP-server hittades i nätverkat.",
"dhcp_found": "Några aktiva DHCP-servar upptäcktes. Det är inte säkert att aktivera inbyggda DHCP-servrar.",
"dhcp_leases": "DHCP-lease",
"dhcp_static_leases": "Statiska DHCP-leases",
"dhcp_leases_not_found": "Ingen DHCP-lease hittad",
"dhcp_config_saved": "Sparade inställningar för DHCP-servern",
"form_error_required": "Obligatoriskt fält",
"form_error_ip_format": "Ogiltigt IPv4-format",
"form_error_mac_format": "Ogiltigt MAC-format",
"form_error_positive": "Måste vara större än noll",
"dhcp_form_gateway_input": "Gateway-IP",
"dhcp_form_subnet_input": "Subnetmask",
@@ -36,6 +36,12 @@
"dhcp_error": "Vi kunde inte avgöra om det finns en till DHCP-server på nätverket.",
"dhcp_static_ip_error": "För att kunna använda en DHCP-server måste det finnas en statisk IP-adress. Vi kunde inte avgöra om nätverksgränssnittet är konfigurerat med en statisk IP-adress. Ställ in en statistik IP-adress manuellt.",
"dhcp_dynamic_ip_found": "Din enhet använder en dynamisk IP-adress för gränssnittet <0>{{interfaceName}}</0>. För att kunna använda DHCP-servern behövs en statisk IP-adress. Din nuvarande IP-adress är <0>{{ipAddress}}</0>. Vi kommer att göra denna IP-adress statisk automatiskt om du trycker på knappen \"Aktivera DHCP\".",
"dhcp_lease_added": "Statisk lease \"{{key}}\" har lagts till",
"dhcp_lease_deleted": "Statisk lease \"{{key}}\" har raderats",
"dhcp_new_static_lease": "Ny statisk lease",
"dhcp_static_leases_not_found": "Inga statiska DHCP-leases hittade",
"dhcp_add_static_lease": "Lägg till statisk lease",
"delete_confirm": "Är du säker på att du vill ta bort \"{{key}}\"?",
"form_enter_hostname": "Skriv in värdnamn",
"error_details": "Felinformation",
"back": "Tiilbaka",
@@ -48,6 +54,7 @@
"address": "adress",
"on": "PÅ",
"off": "AV",
"copyright": "Copyright",
"homepage": "Hemsida",
"report_an_issue": "Rapportera ett problem",
"privacy_policy": "Integritetspolicy",
@@ -62,6 +69,8 @@
"stats_adult": "Blockerade vuxensajter",
"stats_query_domain": "Mest eftersökta domäner",
"for_last_24_hours": "under de senaste 24 timamrna",
"for_last_days": "för den senaste {{count}} dagen",
"for_last_days_plural": "för de senaste {{count}} dagarna",
"no_domains_found": "Inga domäner hittade",
"requests_count": "Förfrågningsantal",
"top_blocked_domains": "Flest blockerade domäner",
@@ -83,11 +92,12 @@
"use_adguard_parental": "Använda AdGuards webbservice för färäldrakontroll",
"use_adguard_parental_hint": "AdGuard Home kommer att kontrollera domäner för innehåll av vuxenmaterial . Samma integritetsvänliga metod för API-lookup som tillämpas i webbservicens surfsäkerhet används.",
"enforce_safe_search": "Tillämpa Säker surf",
"enforce_save_search_hint": "AdGuard Home kan framtvinga säker surf i följande sökmoterer: Google, Youtube, Bing, och Yandex.",
"no_servers_specified": "Inga servrar angivna",
"general_settings": "Allmänna inställningar",
"dns_settings": "DNS-inställningar",
"encryption_settings": "Krypteringsinställningar",
"dhcp_settings": "DHCP-inställningar",
"upstream_dns": "Upstream DNS-servrar",
"upstream_dns_hint": "Om du låter fältet vara tomt kommer AdGuard Home att använda <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> för uppström.",
"test_upstream_btn": "Testa uppströmmar",
"apply_btn": "Tillämpa",
"disabled_filtering_toast": "Filtrering bortkopplad",
@@ -125,6 +135,7 @@
"example_comment": "! Här kommer en kommentar",
"example_comment_meaning": "Endast en kommentar",
"example_comment_hash": "# Också en kommentar",
"example_regex_meaning": "blockera åtkomst till domäner som matchar det angivna uttrycket",
"example_upstream_regular": "vanlig DNS (över UDP)",
"example_upstream_dot": "krypterat <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>",
"example_upstream_doh": "krypterat <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>",
@@ -155,6 +166,17 @@
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna",
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrerat av {{filter}}",
"query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?",
"query_log_cleared": "Förfrågningsloggen har rensats",
"query_log_clear": "Rensa förfrågningsloggar",
"query_log_retention": "Förfrågningsloggars retentionstid",
"query_log_enable": "Aktivera logg",
"query_log_configuration": "Logginställningar",
"query_log_disabled": "Förfrågningsloggen är avaktiverad och kan konfigureras i <0>inställningar</0>",
"query_log_strict_search": "Använd dubbla citattecken för strikt sökning",
"query_log_retention_confirm": "Är du säker på att du vill ändra förfrågningsloggars retentionstid? Om du minskar intervallet kommer viss data att gå förlorad",
"source_label": "Källa",
"found_in_known_domain_db": "Hittad i domändatabas.",
"category_label": "Kategori",
@@ -253,9 +275,14 @@
"dns_addresses": "DNS-adresser",
"down": "Ner",
"fix": "Fixa",
"dns_providers": "Här är en <0>lista över kända DNS-leverantörer</0> att välja från.",
"update_now": "Uppdatera nu",
"update_failed": "Automatisk uppdatering misslyckad. Var god <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>följ stegen</a> för att uppdatera manuellt.",
"processing_update": "Vänta, AdGuard Home uppdateras",
"clients_title": "Klienter",
"clients_desc": "Konfigurera enheter uppkopplade mot AdGuard Home",
"settings_global": "Global",
"settings_custom": "Anpassade",
"table_client": "Klient",
"table_name": "Namn",
"save_btn": "Spara",
@@ -268,8 +295,45 @@
"form_enter_mac": "Skriv in MAC",
"form_client_name": "Skriv in klientnamn",
"client_global_settings": "Använda globala inställningar",
"client_deleted": "Klient \"{{key}}\" har raderats",
"client_added": "Klient \"{{key}}\" har lagts till",
"client_updated": "Klient \"{{key}}\" har uppdaterats",
"clients_not_found": "Inga klienter hittade",
"client_confirm_delete": "Är du säker på att du vill ta bort klient \"{{key}}\"?",
"filter_confirm_delete": "Är du säker på att du ska ta bort filtret?",
"auto_clients_title": "Klienter (körtid)",
"auto_clients_desc": "Data från klienter som använder AdGuard Home, men inte är sparade i konfigurationen",
"access_title": "Åtkomstinställningar",
"access_desc": "Här kan du konfigurera åtkomstregler för AdGuard Homes DNS-server.",
"access_allowed_title": "Tillåtna klienter",
"access_disallowed_title": "Otillåtna klienter",
"access_blocked_title": "Blockerade domäner",
"access_settings_saved": "Åtkomstinställningar sparade",
"updates_checked": "Sökning efter uppdateringar genomförd",
"updates_version_equal": "AdGuard Home är uppdaterat",
"check_updates_now": "Sök efter uppdateringar nu",
"interval_24_hour": "24 timmar",
"domain": "Domän",
"answer": "Svar"
"answer": "Svar",
"statistics_clear": " Rensa statistik",
"interval_hours": "{{count}} timme",
"interval_hours_plural": "{{count}} timmar",
"filters_configuration": "Filterinställningar",
"filters_enable": "Aktivera filter",
"filters_interval": "Filterppdateringsintervall",
"disabled": "Avaktiverad",
"username_label": "Användarnamn",
"username_placeholder": "Skriv in användarnamn",
"password_label": "Lösenord",
"password_placeholder": "Skriv in lösenord",
"sign_in": "Logga in",
"sign_out": "Logga ut",
"forgot_password": "Glömt lösenord?",
"forgot_password_desc": "Följ <0>dessa steg</0> för att skapa ett nytt lösenord till ditt konto.",
"location": "Plats",
"orgname": "Organisationsnamn",
"netname": "Nätverksnamn",
"descr": "Beskrivning",
"whois": "Whois",
"filtering_rules_learn_more": "<0>Mer info</0> om att skapa dina egna blockeringslistor för värdar."
}

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP kiralamaları",
"dhcp_static_leases": "Sabit DHCP kiralamaları",
"dhcp_leases_not_found": "DHCP kiralaması bulunamadı",
"dhcp_config_saved": "DHCP sunucusu ayarı kaydedildi",
"form_error_required": "Gerekli alan",
"form_error_ip_format": "Geçersiz IPv4 formatı",
"form_error_mac_format": "Geçersiz MAC biçimi",
"form_error_positive": "0'dan büyük olmalı",
"dhcp_form_gateway_input": "Ağ Geçidi IP'si",
@@ -96,14 +94,12 @@
"use_adguard_parental": "AdGuard ebeveyn kontrolü web hizmetini kullan",
"use_adguard_parental_hint": "AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol edecek. Gezinti güvenliği web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanıyoruz.",
"enforce_safe_search": "Güvenli aramayı zorunlu kıl",
"enforce_save_search_hint": "AdGuard Home şu arama motorlarında güvenli aramayı zorunlu kılabilir: Google, Youtube, Bing, DuckDuckGo ve Yandex.",
"no_servers_specified": "Sunucu adresi girilmedi",
"general_settings": "Genel ayarlar",
"dns_settings": "DNS ayarları",
"encryption_settings": "Şifreleme ayarları",
"dhcp_settings": "DHCP ayarları",
"upstream_dns": "Üst DNS sunucusu",
"upstream_dns_hint": "Eğer bu alanı boş bırakırsanız AdGuard Home üst sunucu olarak <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> adresini kullanacaktır.",
"test_upstream_btn": "Üst sunucuyu test et",
"apply_btn": "Uygula",
"disabled_filtering_toast": "Filtreleme devre dışı",
@@ -141,7 +137,6 @@
"example_comment": "! Buraya bir yorum ekledim",
"example_comment_meaning": "yorum eklemek",
"example_comment_hash": "# Bir yorum daha ekledim",
"example_regex_meaning": "belirtilen düzenli ifadelerle eşleşen alan adlarına erişimi engelle",
"example_upstream_regular": "normal DNS (UDP üzerinden)",
"example_upstream_dot": "<0>DNS-over-TLS</0> şifrelemesi",
"example_upstream_doh": "<0>DNS-over-HTTPS</0> şifrelemesi",
@@ -287,7 +282,6 @@
"client_edit": "İstemciyi düzenle",
"client_identifier": "Tanımlayıcı",
"ip_address": "IP adresi",
"client_identifier_desc": "İstemciler IP adresleri veya MAC adresleri ile tanımlanabilir. Lütfen not edin, MAC adresi ile tanımlamayı kullanmak için AdGuard Home'un <0>DHCP Sunucusu</0> olması gerekir.",
"form_enter_ip": "IP Girin",
"form_enter_mac": "MAC Girin",
"form_client_name": "İstemci ismi girin",

View File

@@ -55,11 +55,9 @@
"use_adguard_parental": "Sử dụng dịch vụ quản lý của phụ huynh AdGuard",
"use_adguard_parental_hint": "AdGuard Home sẽ kiểm tra nếu tên miền chứa từ khoá người lớn. Tính năng sử dụng API thân thiện với quyền riêng tư tương tự với dịch vụ bảo vệ duyệt web",
"enforce_safe_search": "Bắt buộc tìm kiếm an toàn",
"enforce_save_search_hint": "AdGuard Home có thể bắt buộc tìm kiếm an toàn với các dịch vụ tìm kiếm: Google, Youtube, Bing, Yandex.",
"no_servers_specified": "Không có máy chủ nào được liệt kê",
"general_settings": "Cài đặt chung",
"upstream_dns": "Máy chủ DNS tìm kiếm",
"upstream_dns_hint": "Nếu bạn để trống mục này, AdGuard Home sẽ sử dụng <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> để tìm kiếm. Sử dụng tiền tố tls:// cho các máy chủ DNS dựa trên TLS.",
"test_upstream_btn": "Kiểm tra",
"apply_btn": "Áp dụng",
"disabled_filtering_toast": "Đã tắt chặn quảng cáo",

View File

@@ -17,9 +17,7 @@
"dhcp_leases": "DHCP 租约",
"dhcp_static_leases": "DHCP 静态租约",
"dhcp_leases_not_found": "未检测到 DHCP 租约",
"dhcp_config_saved": "保存 DHCP 服务器配置",
"form_error_required": "必填字段",
"form_error_ip_format": "无效的 IPv4 格式",
"form_error_mac_format": "无效的 MAC 格式",
"form_error_positive": "必须大于 0",
"dhcp_form_gateway_input": "网关 IP",
@@ -90,14 +88,12 @@
"use_adguard_parental": "使用 AdGuard 【家长控制】服务",
"use_adguard_parental_hint": "AdGuard Home 将使用与浏览安全服务相同的隐私性强的 API 来检查域名指向的网站是否包含成人内容。",
"enforce_safe_search": "强制安全搜索",
"enforce_save_search_hint": "AdGuard Home 将对以下搜索引擎强制启用安全搜索Google、YouTube、Bing 和 Yandex。",
"no_servers_specified": "未找到指定的服务器",
"general_settings": "常规设置",
"dns_settings": "DNS 设置",
"encryption_settings": "加密设置",
"dhcp_settings": "DHCP 设置",
"upstream_dns": "上游 DNS 服务器",
"upstream_dns_hint": "如果此处留空AdGuard Home 将会使用 <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> 作为上游 DNS。如果想要使用使用 DNS over TLS请以 tls:// 为开头。",
"test_upstream_btn": "测试上游 DNS",
"apply_btn": "应用",
"disabled_filtering_toast": "过滤器已禁用",
@@ -278,7 +274,6 @@
"client_edit": "编辑客户端",
"client_identifier": "标识符",
"ip_address": "IP 地址",
"client_identifier_desc": "客户端可通过 IP 地址或 MAC 地址识别。请注意,如 AdGuard Home 也是 <0>DHCP 服务器</0>,则仅能将 MAC 用作标识符",
"form_enter_ip": "输入 IP",
"form_enter_mac": "输入 MAC",
"form_client_name": "输入客户端名称",

View File

@@ -17,9 +17,11 @@
"dhcp_leases": "動態主機設定協定DHCP租賃",
"dhcp_static_leases": "動態主機設定協定DHCP靜態租賃",
"dhcp_leases_not_found": "無已發現之動態主機設定協定DHCP租賃",
"dhcp_config_saved": "已儲存動態主機設定協定DHCP伺服器配置",
"dhcp_config_saved": "動態主機設定協定DHCP配置被成功地儲存",
"form_error_required": "必填的欄位",
"form_error_ip_format": "無效的 IPv4 格式",
"form_error_ip4_format": "無效的 IPv4 格式",
"form_error_ip6_format": "無效的 IPv6 格式",
"form_error_ip_format": "無效的 IP 格式",
"form_error_mac_format": "無效的媒體存取控制MAC格式",
"form_error_positive": "必須大於 0",
"dhcp_form_gateway_input": "閘道 IP",
@@ -43,6 +45,7 @@
"dhcp_new_static_lease": "新的靜態租賃",
"dhcp_static_leases_not_found": "無已發現之動態主機設定協定DHCP靜態租賃",
"dhcp_add_static_lease": "增加靜態租賃",
"dhcp_reset": "您確定您想要重置動態主機設定協定DHCP配置嗎",
"delete_confirm": "您確定您想要刪除 \"{{key}}\" 嗎?",
"form_enter_hostname": "輸入主機名稱",
"error_details": "錯誤細節",
@@ -96,15 +99,16 @@
"use_adguard_parental": "使用 AdGuard 家長監控之網路服務",
"use_adguard_parental_hint": "AdGuard Home 將檢查網域是否包含成人資料。它使用如同瀏覽安全網路服務一樣之友好的隱私應用程式介面API。",
"enforce_safe_search": "強制執行安全搜尋",
"enforce_save_search_hint": "AdGuard Home 可在下列的搜尋引擎Google、YouTube、Bing、DuckDuckGoYandex 中強制執行安全搜尋。",
"enforce_save_search_hint": "AdGuard Home 可在下列的搜尋引擎Google、YouTube、Bing、DuckDuckGoYandex 和 Pixabay 中強制執行安全搜尋。",
"no_servers_specified": "無已明確指定的伺服器",
"general_settings": "一般的設定",
"dns_settings": "DNS 設定",
"encryption_settings": "加密設定",
"dhcp_settings": "動態主機設定協定DHCP設定",
"upstream_dns": "上游的 DNS 伺服器",
"upstream_dns_hint": "如果您將該欄位留空AdGuard Home 將使用 <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> 作為上游。",
"upstream_dns_hint": "如果您將該欄位留空AdGuard Home 將使用 <a href='https://www.quad9.net/' target='_blank'>Quad9</a> 作為上游。",
"test_upstream_btn": "測試上行資料流",
"upstreams": "上游",
"apply_btn": "套用",
"disabled_filtering_toast": "已禁用過濾",
"enabled_filtering_toast": "已啟用過濾",
@@ -279,6 +283,8 @@
"update_announcement": "AdGuard Home {{version}} 現為可用的!關於更多的資訊,<0>點擊這裡</0>。",
"setup_guide": "設置指南",
"dns_addresses": "DNS 位址",
"dns_start": "DNS 伺服器正在啟動",
"dns_status_error": "取得 DNS 伺服器狀態之錯誤",
"down": "停止運作的",
"fix": "修復",
"dns_providers": "這裡是一個從中選擇之<0>已知的 DNS 供應商之清單</0>。",
@@ -297,9 +303,11 @@
"client_edit": "編輯用戶端",
"client_identifier": "識別碼",
"ip_address": "IP 位址",
"client_identifier_desc": "用戶端可被 IP 位址或媒體存取控制MAC位址識別。請注意僅若 AdGuard Home 也是<0>動態主機設定協定DHCP伺服器</0>,使用 MAC 作為識別碼是可能的",
"client_identifier_desc": "用戶端可被 IP 位址、無類別網域間路由CIDR或媒體存取控制MAC位址識別。請注意僅若 AdGuard Home 也是<0>動態主機設定協定DHCP伺服器</0>,使用 MAC 作為識別碼是可能的",
"form_enter_ip": "輸入 IP",
"form_enter_mac": "輸入媒體存取控制MAC",
"form_enter_id": "輸入識別碼",
"form_add_id": "增加識別碼",
"form_client_name": "輸入用戶端名稱",
"client_global_settings": "使用全域的設定",
"client_deleted": "用戶端 \"{{key}}\" 被成功地刪除",
@@ -400,5 +408,6 @@
"netname": "網路名稱",
"descr": "說明",
"whois": "Whois",
"filtering_rules_learn_more": "<0>了解更多</0>有關創建您自己的主機hosts封鎖清單。"
"filtering_rules_learn_more": "<0>了解更多</0>有關創建您自己的主機hosts封鎖清單。",
"blocked_by_response": "被正規名稱CNAME或 IP 封鎖作為回應"
}

View File

@@ -2,7 +2,6 @@ import { createAction } from 'redux-actions';
import { t } from 'i18next';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast, getClients } from './index';
import { CLIENT_ID } from '../helpers/constants';
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
@@ -13,18 +12,7 @@ export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
export const addClient = config => async (dispatch) => {
dispatch(addClientRequest());
try {
let data;
if (config.identifier === CLIENT_ID.MAC) {
const { ip, identifier, ...values } = config;
data = { ...values };
} else {
const { mac, identifier, ...values } = config;
data = { ...values };
}
await apiClient.addClient(data);
await apiClient.addClient(config);
dispatch(addClientSuccess());
dispatch(toggleClientModal());
dispatch(addSuccessToast(t('client_added', { key: config.name })));
@@ -59,16 +47,7 @@ export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
export const updateClient = (config, name) => async (dispatch) => {
dispatch(updateClientRequest());
try {
let data;
if (config.identifier === CLIENT_ID.MAC) {
const { ip, identifier, ...values } = config;
data = { name, data: { ...values } };
} else {
const { mac, identifier, ...values } = config;
data = { name, data: { ...values } };
}
const data = { name, data: { ...config } };
await apiClient.updateClient(data);
dispatch(updateClientSuccess());

View File

@@ -0,0 +1,35 @@
import { createAction } from 'redux-actions';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
export const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS');
export const getDnsConfig = () => async (dispatch) => {
dispatch(getDnsConfigRequest());
try {
const data = await apiClient.getDnsConfig();
dispatch(getDnsConfigSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDnsConfigFailure());
}
};
export const setDnsConfigRequest = createAction('SET_DNS_CONFIG_REQUEST');
export const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE');
export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
export const setDnsConfig = config => async (dispatch) => {
dispatch(setDnsConfigRequest());
try {
await apiClient.setDnsConfig(config);
dispatch(addSuccessToast('config_successfully_saved'));
dispatch(setDnsConfigSuccess(config));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDnsConfigFailure());
}
};

View File

@@ -91,17 +91,9 @@ export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS')
export const toggleProtection = status => async (dispatch) => {
dispatch(toggleProtectionRequest());
let successMessage = '';
try {
if (status) {
successMessage = 'disabled_protection';
await apiClient.disableGlobalProtection();
} else {
successMessage = 'enabled_protection';
await apiClient.enableGlobalProtection();
}
const successMessage = status ? 'disabled_protection' : 'enabled_protection';
await apiClient.setDnsConfig({ protection_enabled: !status });
dispatch(addSuccessToast(successMessage));
dispatch(toggleProtectionSuccess());
} catch (error) {
@@ -140,55 +132,65 @@ export const getUpdateRequest = createAction('GET_UPDATE_REQUEST');
export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
const checkStatus = async (handleRequestSuccess, handleRequestError, attempts = 60) => {
let timeout;
if (attempts === 0) {
handleRequestError();
}
const rmTimeout = t => t && clearTimeout(t);
try {
const response = await axios.get('control/status');
rmTimeout(timeout);
if (response && response.status === 200) {
handleRequestSuccess(response);
if (response.data.running === false) {
timeout = setTimeout(
checkStatus,
CHECK_TIMEOUT,
handleRequestSuccess,
handleRequestError,
attempts - 1,
);
}
}
} catch (error) {
rmTimeout(timeout);
timeout = setTimeout(
checkStatus,
CHECK_TIMEOUT,
handleRequestSuccess,
handleRequestError,
attempts - 1,
);
}
};
export const getUpdate = () => async (dispatch, getState) => {
const { dnsVersion } = getState().dashboard;
dispatch(getUpdateRequest());
try {
await apiClient.getUpdate();
const checkUpdate = async (attempts) => {
let count = attempts || 1;
let timeout;
if (count > 60) {
dispatch(addNoticeToast({ error: 'update_failed' }));
dispatch(getUpdateFailure());
return false;
}
const rmTimeout = t => t && clearTimeout(t);
const setRecursiveTimeout = (time, ...args) => setTimeout(
checkUpdate,
time,
...args,
);
axios.get('control/status')
.then((response) => {
rmTimeout(timeout);
if (response && response.status === 200) {
const responseVersion = response.data && response.data.version;
if (dnsVersion !== responseVersion) {
dispatch(getUpdateSuccess());
window.location.reload(true);
}
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
})
.catch(() => {
rmTimeout(timeout);
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
});
return false;
};
checkUpdate();
} catch (error) {
const handleRequestError = () => {
dispatch(addNoticeToast({ error: 'update_failed' }));
dispatch(getUpdateFailure());
};
const handleRequestSuccess = (response) => {
const responseVersion = response.data && response.data.version;
if (dnsVersion !== responseVersion) {
dispatch(getUpdateSuccess());
window.location.reload(true);
}
};
try {
await apiClient.getUpdate();
checkStatus(handleRequestSuccess, handleRequestError);
} catch (error) {
handleRequestError();
}
};
@@ -231,18 +233,34 @@ export const getProfile = () => async (dispatch) => {
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
export const setDnsRunningStatus = createAction('SET_DNS_RUNNING_STATUS');
export const getDnsStatus = () => async (dispatch) => {
dispatch(dnsStatusRequest());
try {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus));
dispatch(getVersion());
dispatch(getTlsStatus());
dispatch(getProfile());
} catch (error) {
dispatch(addErrorToast({ error }));
const handleRequestError = () => {
dispatch(addErrorToast({ error: 'dns_status_error' }));
dispatch(dnsStatusFailure());
window.location.reload(true);
};
const handleRequestSuccess = (response) => {
const dnsStatus = response.data;
const { running } = dnsStatus;
const runningStatus = dnsStatus && running;
if (runningStatus === true) {
dispatch(dnsStatusSuccess(dnsStatus));
dispatch(getVersion());
dispatch(getTlsStatus());
dispatch(getProfile());
} else {
dispatch(setDnsRunningStatus(running));
}
};
try {
checkStatus(handleRequestSuccess, handleRequestError);
} catch (error) {
handleRequestError(error);
}
};
@@ -261,36 +279,6 @@ export const getDnsSettings = () => async (dispatch) => {
}
};
export const enableDnsRequest = createAction('ENABLE_DNS_REQUEST');
export const enableDnsFailure = createAction('ENABLE_DNS_FAILURE');
export const enableDnsSuccess = createAction('ENABLE_DNS_SUCCESS');
export const enableDns = () => async (dispatch) => {
dispatch(enableDnsRequest());
try {
await apiClient.startGlobalFiltering();
dispatch(enableDnsSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(enableDnsFailure());
}
};
export const disableDnsRequest = createAction('DISABLE_DNS_REQUEST');
export const disableDnsFailure = createAction('DISABLE_DNS_FAILURE');
export const disableDnsSuccess = createAction('DISABLE_DNS_SUCCESS');
export const disableDns = () => async (dispatch) => {
dispatch(disableDnsRequest());
try {
await apiClient.stopGlobalFiltering();
dispatch(disableDnsSuccess());
} catch (error) {
dispatch(disableDnsFailure(error));
dispatch(addErrorToast({ error }));
}
};
export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
@@ -470,6 +458,22 @@ export const toggleDhcp = values => async (dispatch) => {
}
};
export const resetDhcpRequest = createAction('RESET_DHCP_REQUEST');
export const resetDhcpSuccess = createAction('RESET_DHCP_SUCCESS');
export const resetDhcpFailure = createAction('RESET_DHCP_FAILURE');
export const resetDhcp = () => async (dispatch) => {
dispatch(resetDhcpRequest());
try {
const status = await apiClient.resetDhcp();
dispatch(resetDhcpSuccess(status));
dispatch(addSuccessToast('dhcp_config_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(resetDhcpFailure());
}
};
export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');
export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');

View File

@@ -2,27 +2,104 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { normalizeLogs } from '../helpers/helpers';
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
const getLogsWithParams = async (config) => {
const { older_than, filter, ...values } = config;
const rawLogs = await apiClient.getQueryLog({ ...filter, older_than });
const { data, oldest } = rawLogs;
const logs = normalizeLogs(data);
const clientsParams = getParamsForClientsSearch(logs, 'client');
const clients = await apiClient.findClients(clientsParams);
const logsWithClientInfo = addClientInfo(logs, clients, 'client');
return {
logs: logsWithClientInfo, oldest, older_than, filter, ...values,
};
};
export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUEST');
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
const checkFilteredLogs = async (data, filter, dispatch, total) => {
const { logs, oldest } = data;
const totalData = total || { logs };
const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE ||
totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE) &&
oldest !== '';
if (needToGetAdditionalLogs) {
dispatch(getAdditionalLogsRequest());
try {
const additionalLogs = await getLogsWithParams({ older_than: oldest, filter });
if (additionalLogs.logs.length > 0) {
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
logs: [...totalData.logs, ...additionalLogs.logs],
oldest: additionalLogs.oldest,
});
}
dispatch(getAdditionalLogsSuccess());
return totalData;
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getAdditionalLogsFailure(error));
}
}
dispatch(getAdditionalLogsSuccess());
return totalData;
};
export const setLogsPagination = createAction('LOGS_PAGINATION');
export const setLogsFilter = createAction('LOGS_FILTER');
export const setLogsPage = createAction('SET_LOG_PAGE');
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = config => async (dispatch) => {
export const getLogs = config => async (dispatch, getState) => {
dispatch(getLogsRequest());
try {
const { filter, lastRowTime: older_than } = config;
const logs = normalizeLogs(await apiClient.getQueryLog({ ...filter, older_than }));
dispatch(getLogsSuccess({ logs, ...config }));
const { isFiltered, filter, page } = getState().queryLogs;
const data = await getLogsWithParams({ ...config, filter });
if (isFiltered) {
const additionalData = await checkFilteredLogs(data, filter, dispatch);
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
dispatch(getLogsSuccess(updatedData));
dispatch(setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE }));
} else {
dispatch(getLogsSuccess(data));
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
}
};
export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
export const setLogsFilterFailure = createAction('SET_LOGS_FILTER_FAILURE');
export const setLogsFilterSuccess = createAction('SET_LOGS_FILTER_SUCCESS');
export const setLogsFilter = filter => async (dispatch) => {
dispatch(setLogsFilterRequest());
try {
const data = await getLogsWithParams({ older_than: '', filter });
const additionalData = await checkFilteredLogs(data, filter, dispatch);
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
dispatch(setLogsFilterSuccess({ ...updatedData, filter }));
dispatch(setLogsPage(0));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setLogsFilterFailure(error));
}
};
export const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');

View File

@@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { normalizeTopStats, secondsToMilliseconds } from '../helpers/helpers';
import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
@@ -43,11 +43,15 @@ export const getStats = () => async (dispatch) => {
dispatch(getStatsRequest());
try {
const stats = await apiClient.getStats();
const normalizedTopClients = normalizeTopStats(stats.top_clients);
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
const clients = await apiClient.findClients(clientsParams);
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
const normalizedStats = {
...stats,
top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),
top_clients: normalizeTopStats(stats.top_clients),
top_clients: topClientsWithInfo,
top_queried_domains: normalizeTopStats(stats.top_queried_domains),
avg_processing_time: secondsToMilliseconds(stats.avg_processing_time),
};

View File

@@ -24,26 +24,12 @@ class Api {
}
// Global methods
GLOBAL_START = { path: 'start', method: 'POST' };
GLOBAL_STATUS = { path: 'status', method: 'GET' };
GLOBAL_STOP = { path: 'stop', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
startGlobalFiltering() {
const { path, method } = this.GLOBAL_START;
return this.makeRequest(path, method);
}
stopGlobalFiltering() {
const { path, method } = this.GLOBAL_STOP;
return this.makeRequest(path, method);
}
getGlobalStatus() {
const { path, method } = this.GLOBAL_STATUS;
return this.makeRequest(path, method);
@@ -76,16 +62,6 @@ class Api {
return this.makeRequest(path, method, config);
}
enableGlobalProtection() {
const { path, method } = this.GLOBAL_ENABLE_PROTECTION;
return this.makeRequest(path, method);
}
disableGlobalProtection() {
const { path, method } = this.GLOBAL_DISABLE_PROTECTION;
return this.makeRequest(path, method);
}
getUpdate() {
const { path, method } = this.GLOBAL_UPDATE;
return this.makeRequest(path, method);
@@ -248,6 +224,7 @@ class Api {
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
DHCP_RESET = { path: 'dhcp/reset', method: 'POST' };
getDhcpStatus() {
const { path, method } = this.DHCP_STATUS;
@@ -295,6 +272,11 @@ class Api {
return this.makeRequest(path, method, parameters);
}
resetDhcp() {
const { path, method } = this.DHCP_RESET;
return this.makeRequest(path, method);
}
// Installation
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
@@ -353,6 +335,7 @@ class Api {
// Per-client settings
GET_CLIENTS = { path: 'clients', method: 'GET' };
FIND_CLIENTS = { path: 'clients/find', method: 'GET' };
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
@@ -389,6 +372,12 @@ class Api {
return this.makeRequest(path, method, parameters);
}
findClients(params) {
const { path, method } = this.FIND_CLIENTS;
const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method);
}
// DNS access settings
ACCESS_LIST = { path: 'access/list', method: 'GET' };
ACCESS_SET = { path: 'access/set', method: 'POST' };
@@ -533,6 +522,24 @@ class Api {
const { path, method } = this.GET_PROFILE;
return this.makeRequest(path, method);
}
// DNS config
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' };
getDnsConfig() {
const { path, method } = this.GET_DNS_CONFIG;
return this.makeRequest(path, method);
}
setDnsConfig(data) {
const { path, method } = this.SET_DNS_CONFIG;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
}
const apiClient = new Api();

View File

@@ -29,6 +29,7 @@ import UpdateOverlay from '../ui/UpdateOverlay';
import EncryptionTopline from '../ui/EncryptionTopline';
import Icons from '../ui/Icons';
import i18n from '../../i18n';
import Loading from '../ui/Loading';
class App extends Component {
componentDidMount() {
@@ -41,8 +42,8 @@ class App extends Component {
}
}
handleStatusChange = () => {
this.props.enableDns();
reloadPage = () => {
window.location.reload();
};
handleUpdate = () => {
@@ -88,10 +89,14 @@ class App extends Component {
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
{!dashboard.processing && !dashboard.isCoreRunning && (
{dashboard.processing && <Loading />}
{!dashboard.isCoreRunning && (
<div className="row row-cards">
<div className="col-lg-12">
<Status handleStatusChange={this.handleStatusChange} />
<Status reloadPage={this.reloadPage}
message="dns_start"
/>
<Loading />
</div>
</div>
)}

View File

@@ -28,19 +28,17 @@ const countCell = dnsQueries =>
return <Cell value={value} percent={percent} color={percentColor} />;
};
const clientCell = (clients, autoClients, t) =>
const clientCell = t =>
function cell(row) {
const { value } = row;
return (
<div className="logs__row logs__row--overflow logs__row--column">
{formatClientCell(value, clients, autoClients, t)}
{formatClientCell(row, t)}
</div>
);
};
const Clients = ({
t, refreshButton, topClients, subtitle, clients, autoClients, dnsQueries,
t, refreshButton, topClients, subtitle, dnsQueries,
}) => (
<Card
title={t('top_clients')}
@@ -49,9 +47,10 @@ const Clients = ({
refresh={refreshButton}
>
<ReactTable
data={topClients.map(({ name: ip, count }) => ({
data={topClients.map(({ name: ip, count, info }) => ({
ip,
count,
info,
}))}
columns={[
{
@@ -59,7 +58,7 @@ const Clients = ({
accessor: 'ip',
sortMethod: (a, b) =>
parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
Cell: clientCell(clients, autoClients, t),
Cell: clientCell(t),
},
{
Header: <Trans>requests_count</Trans>,

View File

@@ -20,7 +20,6 @@ class Dashboard extends Component {
getAllStats = () => {
this.props.getStats();
this.props.getStatsConfig();
this.props.getClients();
};
getToggleFilteringButton = () => {
@@ -42,11 +41,7 @@ class Dashboard extends Component {
render() {
const { dashboard, stats, t } = this.props;
const dashboardProcessing =
dashboard.processing ||
dashboard.processingClients ||
stats.processingStats ||
stats.processingGetConfig;
const statsProcessing = stats.processingStats || stats.processingGetConfig;
const subtitle =
stats.interval === 1
@@ -83,8 +78,8 @@ class Dashboard extends Component {
{refreshFullButton}
</div>
</PageTitle>
{dashboardProcessing && <Loading />}
{!dashboardProcessing && (
{statsProcessing && <Loading />}
{!statsProcessing && (
<div className="row row-cards">
<div className="col-lg-12">
<Statistics

View File

@@ -153,7 +153,7 @@ class Filters extends Component {
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
ofText="/"
rowsText={t('rows_table_footer_text')}
noDataText={t('no_filters_added')}
/>

View File

@@ -0,0 +1,116 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import { renderField } from '../../../helpers/form';
import { RESPONSE_FILTER } from '../../../helpers/constants';
import Tooltip from '../../ui/Tooltip';
const renderFilterField = ({
input,
id,
className,
placeholder,
type,
disabled,
autoComplete,
tooltip,
meta: { touched, error },
}) => (
<Fragment>
<div className="logs__input-wrap">
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
/>
<span className="logs__notice">
<Tooltip text={tooltip} type='tooltip-custom--logs' />
</span>
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</div>
</Fragment>
);
const Form = (props) => {
const {
t,
handleChange,
} = props;
return (
<form onSubmit={handleChange}>
<div className="row">
<div className="col-6 col-sm-3 my-2">
<Field
id="filter_domain"
name="filter_domain"
component={renderFilterField}
type="text"
className="form-control"
placeholder={t('domain_name_table_header')}
tooltip={t('query_log_strict_search')}
onChange={handleChange}
/>
</div>
<div className="col-6 col-sm-3 my-2">
<Field
id="filter_question_type"
name="filter_question_type"
component={renderField}
type="text"
className="form-control"
placeholder={t('type_table_header')}
onChange={handleChange}
/>
</div>
<div className="col-6 col-sm-3 my-2">
<Field
name="filter_response_status"
component="select"
className="form-control custom-select"
>
<option value={RESPONSE_FILTER.ALL}>
<Trans>show_all_filter_type</Trans>
</option>
<option value={RESPONSE_FILTER.FILTERED}>
<Trans>show_filtered_type</Trans>
</option>
</Field>
</div>
<div className="col-6 col-sm-3 my-2">
<Field
id="filter_client"
name="filter_client"
component={renderFilterField}
type="text"
className="form-control"
placeholder={t('client_table_header')}
tooltip={t('query_log_strict_search')}
onChange={handleChange}
/>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleChange: PropTypes.func,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({
form: 'logsFilterForm',
}),
])(Form);

View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import classnames from 'classnames';
import { DEBOUNCE_FILTER_TIMEOUT, RESPONSE_FILTER } from '../../../helpers/constants';
import { isValidQuestionType } from '../../../helpers/helpers';
import Form from './Form';
import Card from '../../ui/Card';
class Filters extends Component {
getFilters = ({
filter_domain, filter_question_type, filter_response_status, filter_client,
}) => ({
filter_domain: filter_domain || '',
filter_question_type: isValidQuestionType(filter_question_type) ? filter_question_type.toUpperCase() : '',
filter_response_status: filter_response_status === RESPONSE_FILTER.FILTERED ? filter_response_status : '',
filter_client: filter_client || '',
});
handleFormChange = debounce((values) => {
const filter = this.getFilters(values);
this.props.setLogsFilter(filter);
}, DEBOUNCE_FILTER_TIMEOUT);
render() {
const { filter, processingAdditionalLogs } = this.props;
const cardBodyClass = classnames({
'card-body': true,
'card-body--loading': processingAdditionalLogs,
});
return (
<Card bodyType={cardBodyClass}>
<Form
initialValues={filter}
onChange={this.handleFormChange}
/>
</Card>
);
}
}
Filters.propTypes = {
filter: PropTypes.object.isRequired,
setLogsFilter: PropTypes.func.isRequired,
processingGetLogs: PropTypes.bool.isRequired,
processingAdditionalLogs: PropTypes.bool.isRequired,
};
export default Filters;

View File

@@ -5,46 +5,39 @@ import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link';
import debounce from 'lodash/debounce';
import {
formatTime,
formatDateTime,
isValidQuestionType,
} from '../../helpers/helpers';
import { SERVICES, FILTERED_STATUS, DEBOUNCE_TIMEOUT, DEFAULT_LOGS_FILTER } from '../../helpers/constants';
import { SERVICES, FILTERED_STATUS, TABLE_DEFAULT_PAGE_SIZE } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers';
import { formatClientCell } from '../../helpers/formatClientCell';
import Filters from './Filters';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Loading from '../ui/Loading';
import PopoverFiltered from '../ui/PopoverFilter';
import Popover from '../ui/Popover';
import Tooltip from '../ui/Tooltip';
import './Logs.css';
const TABLE_FIRST_PAGE = 0;
const TABLE_DEFAULT_PAGE_SIZE = 50;
const INITIAL_REQUEST_DATA = ['', DEFAULT_LOGS_FILTER, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE];
const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE];
const FILTERED_REASON = 'Filtered';
const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};
class Logs extends Component {
componentDidMount() {
this.props.setLogsPage(TABLE_FIRST_PAGE);
this.getLogs(...INITIAL_REQUEST_DATA);
this.props.getFilteringStatus();
this.props.getClients();
this.props.getLogsConfig();
}
getLogs = (lastRowTime, filter, page, pageSize, filtered) => {
getLogs = (older_than, page) => {
if (this.props.queryLogs.enabled) {
this.props.getLogs({
lastRowTime, filter, page, pageSize, filtered,
older_than, page, pageSize: TABLE_DEFAULT_PAGE_SIZE,
});
}
};
@@ -53,16 +46,6 @@ class Logs extends Component {
window.location.reload();
};
handleLogsFiltering = debounce((lastRowTime, filter, page, pageSize, filtered) => {
this.props.getLogs({
lastRowTime,
filter,
page,
pageSize,
filtered,
});
}, DEBOUNCE_TIMEOUT);
renderTooltip = (isFiltered, rule, filter, service) =>
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
@@ -151,9 +134,16 @@ class Logs extends Component {
);
};
normalizeResponse = response => (
response.map((response) => {
const { value, type, ttl } = response;
return `${type}: ${value} (ttl=${ttl})`;
})
);
getResponseCell = ({ value: responses, original }) => {
const {
reason, filterId, rule, status,
reason, filterId, rule, status, originalAnswer,
} = original;
const { t, filtering } = this.props;
const { filters } = filtering;
@@ -166,6 +156,7 @@ class Logs extends Component {
const isBlockedService = reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const currentService = SERVICES.find(service => service.id === original.serviceName);
const serviceName = currentService && currentService.name;
const normalizedAnswer = originalAnswer && this.normalizeResponse(originalAnswer);
let filterName = '';
if (filterId === 0) {
@@ -185,7 +176,12 @@ class Logs extends Component {
return (
<div className="logs__row logs__row--column">
<div className="logs__text-wrap">
{(isFiltered || isBlockedService) && (
{originalAnswer && (
<span className="logs__text">
<Trans>blocked_by_response</Trans>
</span>
)}
{!originalAnswer && (isFiltered || isBlockedService) && (
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
@@ -200,16 +196,19 @@ class Logs extends Component {
)}
</div>
<div className="logs__list-wrap">
{this.renderResponseList(responses, status)}
{originalAnswer
? this.renderResponseList(normalizedAnswer, status)
: this.renderResponseList(responses, status)
}
{isWhiteList && this.renderTooltip(isWhiteList, rule, filterName)}
</div>
</div>
);
};
getClientCell = ({ original, value }) => {
const { dashboard, t } = this.props;
const { clients, autoClients } = dashboard;
getClientCell = (row) => {
const { original } = row;
const { t } = this.props;
const { reason, domain } = original;
const isFiltered = this.checkFiltered(reason);
const isRewrite = this.checkRewrite(reason);
@@ -217,7 +216,7 @@ class Logs extends Component {
return (
<Fragment>
<div className="logs__row logs__row--overflow logs__row--column">
{formatClientCell(value, clients, autoClients, t)}
{formatClientCell(row, t)}
</div>
{isRewrite ? (
<div className="logs__action">
@@ -232,81 +231,33 @@ class Logs extends Component {
);
};
getFilterInput = ({ filter, onChange }) => (
<Fragment>
<div className="logs__input-wrap">
<input
type="text"
className="form-control"
onChange={event => onChange(event.target.value)}
value={filter ? filter.value : ''}
/>
<span className="logs__notice">
<Tooltip text={this.props.t('query_log_strict_search')} type='tooltip-custom--logs' />
</span>
</div>
</Fragment>
);
getFilters = (filtered) => {
const filteredObj = filtered.reduce((acc, cur) => ({ ...acc, [cur.id]: cur.value }), {});
const {
domain, client, type, response,
} = filteredObj;
return {
filter_domain: domain || '',
filter_client: client || '',
filter_question_type: isValidQuestionType(type) ? type.toUpperCase() : '',
filter_response_status: response === RESPONSE_FILTER.FILTERED ? response : '',
};
};
fetchData = (state) => {
const { pageSize, page, pages } = state;
const { allLogs, filter } = this.props.queryLogs;
const { pages } = state;
const { oldest, page } = this.props.queryLogs;
const isLastPage = pages && (page + 1 === pages);
if (isLastPage) {
const lastRow = allLogs[allLogs.length - 1];
const lastRowTime = (lastRow && lastRow.time) || '';
this.getLogs(lastRowTime, filter, page, pageSize, true);
} else {
this.props.setLogsPagination({ page, pageSize });
this.getLogs(oldest, page);
}
};
handleFilterChange = (filtered) => {
const filters = this.getFilters(filtered);
this.props.setLogsFilter(filters);
this.handleLogsFiltering('', filters, TABLE_FIRST_PAGE, TABLE_DEFAULT_PAGE_SIZE, true);
}
showTotalPagesCount = (pages) => {
const { total, isEntireLog } = this.props.queryLogs;
const showEllipsis = !isEntireLog && total >= 500;
return (
<span className="-totalPages">
{pages || 1}{showEllipsis && '…' }
</span>
);
}
changePage = (page) => {
this.props.setLogsPage(page);
this.props.setLogsPagination({ page, pageSize: TABLE_DEFAULT_PAGE_SIZE });
};
renderLogs() {
const { queryLogs, dashboard, t } = this.props;
const { processingClients } = dashboard;
const { queryLogs, t } = this.props;
const {
processingGetLogs, processingGetConfig, logs, pages,
processingGetLogs, processingGetConfig, logs, pages, page,
} = queryLogs;
const isLoading = processingGetLogs || processingClients || processingGetConfig;
const isLoading = processingGetLogs || processingGetConfig;
const columns = [
{
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 100,
filterable: false,
Cell: this.getTimeCell,
},
{
@@ -314,7 +265,6 @@ class Logs extends Component {
accessor: 'domain',
minWidth: 180,
Cell: this.getDomainCell,
Filter: this.getFilterInput,
},
{
Header: t('type_table_header'),
@@ -326,28 +276,6 @@ class Logs extends Component {
accessor: 'response',
minWidth: 250,
Cell: this.getResponseCell,
filterMethod: (filter, row) => {
if (filter.value === RESPONSE_FILTER.FILTERED) {
// eslint-disable-next-line no-underscore-dangle
const { reason } = row._original;
return this.checkFiltered(reason) || this.checkWhiteList(reason);
}
return true;
},
Filter: ({ filter, onChange }) => (
<select
className="form-control custom-select"
onChange={event => onChange(event.target.value)}
value={filter ? filter.value : RESPONSE_FILTER.ALL}
>
<option value={RESPONSE_FILTER.ALL}>
<Trans>show_all_filter_type</Trans>
</option>
<option value={RESPONSE_FILTER.FILTERED}>
<Trans>show_filtered_type</Trans>
</option>
</select>
),
},
{
Header: t('client_table_header'),
@@ -355,34 +283,36 @@ class Logs extends Component {
maxWidth: 240,
minWidth: 240,
Cell: this.getClientCell,
Filter: this.getFilterInput,
},
];
return (
<ReactTable
manual
filterable
minRows={5}
page={page}
pages={pages}
columns={columns}
filterable={false}
sortable={false}
data={logs || []}
loading={isLoading}
showPageJump={false}
onFetchData={this.fetchData}
onFilteredChange={this.handleFilterChange}
className="logs__table"
showPagination={true}
showPaginationTop={true}
showPageJump={false}
showPageSizeOptions={false}
onFetchData={this.fetchData}
onPageChange={this.changePage}
className="logs__table"
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
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_logs_found')}
renderTotalPagesCount={this.showTotalPagesCount}
pageText={''}
ofText={''}
renderTotalPagesCount={() => false}
defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id;
return row[id] !== undefined
@@ -426,7 +356,9 @@ class Logs extends Component {
render() {
const { queryLogs, t } = this.props;
const { enabled, processingGetConfig } = queryLogs;
const {
enabled, processingGetConfig, processingAdditionalLogs, processingGetLogs,
} = queryLogs;
const refreshButton = enabled ? (
<button
@@ -446,7 +378,17 @@ class Logs extends Component {
<Fragment>
<PageTitle title={t('query_log')}>{refreshButton}</PageTitle>
{enabled && processingGetConfig && <Loading />}
{enabled && !processingGetConfig && <Card>{this.renderLogs()}</Card>}
{enabled && !processingGetConfig && (
<Fragment>
<Filters
filter={queryLogs.filter}
processingGetLogs={processingGetLogs}
processingAdditionalLogs={processingAdditionalLogs}
setLogsFilter={this.props.setLogsFilter}
/>
<Card>{this.renderLogs()}</Card>
</Fragment>
)}
{!enabled && !processingGetConfig && (
<Card>
<div className="lead text-center py-6">
@@ -479,6 +421,7 @@ Logs.propTypes = {
getLogsConfig: PropTypes.func.isRequired,
setLogsPagination: PropTypes.func.isRequired,
setLogsFilter: PropTypes.func.isRequired,
setLogsPage: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};

View File

@@ -88,7 +88,7 @@ class AutoClients extends Component {
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
ofText="/"
rowsText={t('rows_table_footer_text')}
noDataText={t('clients_not_found')}
/>

View File

@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import ReactTable from 'react-table';
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
import { MODAL_TYPE } from '../../../helpers/constants';
import { normalizeTextarea } from '../../../helpers/helpers';
import Card from '../../ui/Card';
import Modal from './Modal';
import WrapCell from './WrapCell';
@@ -20,13 +21,20 @@ class ClientsTable extends Component {
};
handleSubmit = (values) => {
let config = values;
const config = values;
if (values && values.blocked_services) {
const blocked_services = Object
.keys(values.blocked_services)
.filter(service => values.blocked_services[service]);
config = { ...values, blocked_services };
if (values) {
if (values.blocked_services) {
config.blocked_services = Object
.keys(values.blocked_services)
.filter(service => values.blocked_services[service]);
}
if (values.upstreams && typeof values.upstreams === 'string') {
config.upstreams = normalizeTextarea(values.upstreams);
} else {
config.upstreams = [];
}
}
if (this.props.modalType === MODAL_TYPE.EDIT) {
@@ -40,27 +48,24 @@ class ClientsTable extends Component {
const client = clients.find(item => name === item.name);
if (client) {
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
const { upstreams, whois_info, ...values } = client;
return {
identifier,
use_global_settings: true,
use_global_blocked_services: true,
...client,
upstreams: (upstreams && upstreams.join('\n')) || '',
...values,
};
}
return {
identifier: CLIENT_ID.IP,
ids: [''],
use_global_settings: true,
use_global_blocked_services: true,
};
};
getStats = (ip, stats) => {
getStats = (name, stats) => {
if (stats) {
const statsForCurrentIP = stats.find(item => item.name === ip);
return statsForCurrentIP && statsForCurrentIP.count;
const currentStats = stats.find(item => item.info && item.info.name === name);
return currentStats && currentStats.count;
}
return '';
@@ -76,28 +81,22 @@ class ClientsTable extends Component {
columns = [
{
Header: this.props.t('table_client'),
accessor: 'ip',
accessor: 'ids',
minWidth: 150,
Cell: (row) => {
if (row.original && row.original.mac) {
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={row.original.mac}>
{row.original.mac} <em>(MAC)</em>
</span>
</div>
);
} else if (row.value) {
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text" title={row.value}>
{row.value} <em>(IP)</em>
</span>
</div>
);
}
const { value } = row;
return '';
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text">
{value.map(address => (
<div key={address} title={address}>
{address}
</div>
))}
</span>
</div>
);
},
},
{
@@ -119,9 +118,7 @@ class ClientsTable extends Component {
return (
<div className="logs__row logs__row--overflow">
<div className="logs__text" title={title}>
{title}
</div>
<div className="logs__text">{title}</div>
</div>
);
},
@@ -154,6 +151,24 @@ class ClientsTable extends Component {
);
},
},
{
Header: this.props.t('upstreams'),
accessor: 'upstreams',
minWidth: 120,
Cell: ({ value }) => {
const title = value && value.length > 0 ? (
<Trans>settings_custom</Trans>
) : (
<Trans>settings_global</Trans>
);
return (
<div className="logs__row logs__row--overflow">
<div className="logs__text">{title}</div>
</div>
);
},
},
{
Header: this.props.t('whois'),
accessor: 'whois_info',
@@ -165,8 +180,8 @@ class ClientsTable extends Component {
accessor: 'statistics',
minWidth: 120,
Cell: (row) => {
const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topClients);
const { name } = row.original;
const clientStats = this.getStats(name, this.props.topClients);
if (clientStats) {
return (
@@ -258,7 +273,7 @@ class ClientsTable extends Component {
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
ofText="/"
rowsText={t('rows_table_footer_text')}
noDataText={t('clients_not_found')}
/>

View File

@@ -1,14 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { toggleAllServices } from '../../../helpers/helpers';
import { renderField, renderRadioField, renderSelectField, renderServiceField, ipv4, mac, required } from '../../../helpers/form';
import { CLIENT_ID, SERVICES } from '../../../helpers/constants';
import {
renderField,
renderGroupField,
renderSelectField,
renderServiceField,
} from '../../../helpers/form';
import { SERVICES } from '../../../helpers/constants';
import './Service.css';
const settingsCheckboxes = [
@@ -34,6 +41,67 @@ const settingsCheckboxes = [
},
];
const validate = (values) => {
const errors = {};
const { name, ids } = values;
if (!name || !name.length) {
errors.name = i18n.t('form_error_required');
}
if (ids && ids.length) {
const idArrayErrors = [];
ids.forEach((id, idx) => {
if (!id || !id.length) {
idArrayErrors[idx] = i18n.t('form_error_required');
}
});
if (idArrayErrors.length) {
errors.ids = idArrayErrors;
}
}
return errors;
};
const renderFields = (placeholder, buttonTitle) =>
function cell(row) {
const {
fields,
meta: { error },
} = row;
return (
<div className="form__group">
{fields.map((ip, index) => (
<div key={index} className="mb-1">
<Field
name={ip}
component={renderGroupField}
type="text"
className="form-control"
placeholder={placeholder}
isActionAvailable={index !== 0}
removeField={() => fields.remove(index)}
/>
</div>
))}
<button
type="button"
className="btn btn-link btn-block btn-sm"
onClick={() => fields.push()}
title={buttonTitle}
>
<svg className="icon icon--close">
<use xlinkHref="#plus" />
</svg>
</button>
{error && <div className="error">{error}</div>}
</div>
);
};
let Form = (props) => {
const {
t,
@@ -42,92 +110,53 @@ let Form = (props) => {
change,
pristine,
submitting,
clientIdentifier,
useGlobalSettings,
useGlobalServices,
toggleClientModal,
processingAdding,
processingUpdating,
invalid,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group">
<div className="form__inline mb-2">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
<div className="custom-controls-stacked">
<Field
name="identifier"
component={renderRadioField}
type="radio"
className="form-control mr-2"
value="ip"
placeholder={t('ip_address')}
/>
<Field
name="identifier"
component={renderRadioField}
type="radio"
className="form-control mr-2"
value="mac"
placeholder="MAC"
/>
<div className="form__group mb-0">
<div className="form__group">
<Field
id="name"
name="name"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
/>
</div>
<div className="form__group">
<div className="form__label">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
</div>
<div className="form__desc mt-0">
<Trans
components={[
<a href="#dhcp" key="0">
link
</a>,
]}
>
client_identifier_desc
</Trans>
</div>
</div>
<div className="row">
<div className="col col-sm-6">
{clientIdentifier === CLIENT_ID.IP && (
<div className="form__group">
<Field
id="ip"
name="ip"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
/>
</div>
)}
{clientIdentifier === CLIENT_ID.MAC && (
<div className="form__group">
<Field
id="mac"
name="mac"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_mac')}
validate={[mac, required]}
/>
</div>
)}
</div>
<div className="col col-sm-6">
<Field
id="name"
name="name"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
validate={[required]}
/>
</div>
</div>
<div className="form__desc">
<Trans
components={[
<a href="#dhcp" key="0">
link
</a>,
]}
>
client_identifier_desc
</Trans>
<div className="form__group">
<FieldArray
name="ids"
component={renderFields(t('form_enter_id'), t('form_add_id'))}
/>
</div>
</div>
@@ -140,7 +169,11 @@ let Form = (props) => {
type="checkbox"
component={renderSelectField}
placeholder={t(setting.placeholder)}
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
disabled={
setting.name !== 'use_global_settings'
? useGlobalSettings
: false
}
/>
</div>
))}
@@ -191,6 +224,22 @@ let Form = (props) => {
</div>
</div>
</div>
<div label="upstream" title={props.t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans components={[<a href="#dns" key="0">link</a>]}>
upstream_dns_client_desc
</Trans>
</div>
<Field
id="upstreams"
name="upstreams"
component="textarea"
type="text"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
/>
<Examples />
</div>
</Tabs>
</div>
@@ -210,7 +259,13 @@ let Form = (props) => {
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdding || processingUpdating}
disabled={
submitting ||
invalid ||
pristine ||
processingAdding ||
processingUpdating
}
>
<Trans>save_btn</Trans>
</button>
@@ -227,22 +282,20 @@ Form.propTypes = {
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
toggleClientModal: PropTypes.func.isRequired,
clientIdentifier: PropTypes.string,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
};
const selector = formValueSelector('clientForm');
Form = connect((state) => {
const clientIdentifier = selector(state, 'identifier');
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
return {
clientIdentifier,
useGlobalSettings,
useGlobalServices,
};
@@ -253,5 +306,6 @@ export default flow([
reduxForm({
form: 'clientForm',
enableReinitialize: true,
validate,
}),
])(Form);

View File

@@ -50,6 +50,23 @@ const renderInterfaceValues = (interfaceValues => (
</ul>
));
const clearFields = (change, resetDhcp, t) => {
const fields = {
interface_name: '',
gateway_ip: '',
subnet_mask: '',
range_start: '',
range_end: '',
lease_duration: 86400,
};
// eslint-disable-next-line no-alert
if (window.confirm(t('dhcp_reset'))) {
Object.keys(fields).forEach(field => change(field, fields[field]));
resetDhcp();
}
};
let Form = (props) => {
const {
t,
@@ -61,6 +78,8 @@ let Form = (props) => {
interfaceValue,
processingConfig,
processingInterfaces,
resetDhcp,
change,
} = props;
return (
@@ -160,31 +179,42 @@ let Form = (props) => {
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingConfig}
>
{t('save_config')}
</button>
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingConfig}
>
{t('save_config')}
</button>
<button
type="button"
className="btn btn-secondary btn-standart"
disabled={submitting || processingConfig}
onClick={() => clearFields(change, resetDhcp, t)}
>
<Trans>reset_settings</Trans>
</button>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
interfaces: PropTypes.object,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
interfaces: PropTypes.object.isRequired,
interfaceValue: PropTypes.string,
initialValues: PropTypes.object,
processingConfig: PropTypes.bool,
processingInterfaces: PropTypes.bool,
enabled: PropTypes.bool,
t: PropTypes.func,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
processingInterfaces: PropTypes.bool.isRequired,
enabled: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
resetDhcp: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
};
const selector = formValueSelector('dhcpForm');
Form = connect((state) => {

View File

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withNamespaces } from 'react-i18next';
import { SMALL_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
class Leases extends Component {
cellWrap = ({ value }) => (
@@ -36,7 +37,9 @@ class Leases extends Component {
Cell: this.cellWrap,
},
]}
showPagination={false}
pageSize={SMALL_TABLE_DEFAULT_PAGE_SIZE}
showPageSizeOptions={false}
showPagination={leases.length > SMALL_TABLE_DEFAULT_PAGE_SIZE}
noDataText={t('dhcp_leases_not_found')}
minRows={6}
className="-striped -highlight card-table-overflow"

View File

@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withNamespaces } from 'react-i18next';
import { SMALL_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants';
import Modal from './Modal';
@@ -16,7 +17,7 @@ class StaticLeases extends Component {
handleSubmit = (data) => {
this.props.addStaticLease(data);
}
};
handleDelete = (ip, mac, hostname = '') => {
const name = hostname || ip;
@@ -24,7 +25,7 @@ class StaticLeases extends Component {
if (window.confirm(this.props.t('delete_confirm', { key: name }))) {
this.props.removeStaticLease({ ip, mac, hostname });
}
}
};
render() {
const {
@@ -74,7 +75,7 @@ class StaticLeases extends Component {
}
>
<svg className="icons">
<use xlinkHref="#delete" />
<use xlinkHref="#delete"/>
</svg>
</button>
</div>
@@ -82,7 +83,9 @@ class StaticLeases extends Component {
},
},
]}
showPagination={false}
pageSize={SMALL_TABLE_DEFAULT_PAGE_SIZE}
showPageSizeOptions={false}
showPagination={staticLeases.length > SMALL_TABLE_DEFAULT_PAGE_SIZE}
noDataText={t('dhcp_static_leases_not_found')}
className="-striped -highlight card-table-overflow"
minRows={6}

View File

@@ -154,7 +154,15 @@ class Dhcp extends Component {
};
render() {
const { t, dhcp } = this.props;
const {
t,
dhcp,
resetDhcp,
findActiveDhcp,
addStaticLease,
removeStaticLease,
toggleLeaseModal,
} = this.props;
const statusButtonClass = classnames({
'btn btn-primary btn-standard': true,
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
@@ -184,6 +192,7 @@ class Dhcp extends Component {
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
resetDhcp={resetDhcp}
/>
<hr />
<div className="card-actions mb-3">
@@ -191,9 +200,7 @@ class Dhcp extends Component {
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(interface_name)
}
onClick={() => findActiveDhcp(interface_name)}
disabled={
enabled || !interface_name || dhcp.processingConfig
}
@@ -232,9 +239,9 @@ class Dhcp extends Component {
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={this.props.addStaticLease}
removeStaticLease={this.props.removeStaticLease}
toggleLeaseModal={this.props.toggleLeaseModal}
addStaticLease={addStaticLease}
removeStaticLease={removeStaticLease}
toggleLeaseModal={toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
/>
@@ -243,7 +250,7 @@ class Dhcp extends Component {
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => this.props.toggleLeaseModal()}
onClick={() => toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
@@ -260,16 +267,17 @@ class Dhcp extends Component {
}
Dhcp.propTypes = {
dhcp: PropTypes.object,
toggleDhcp: PropTypes.func,
getDhcpStatus: PropTypes.func,
setDhcpConfig: PropTypes.func,
findActiveDhcp: PropTypes.func,
addStaticLease: PropTypes.func,
removeStaticLease: PropTypes.func,
toggleLeaseModal: PropTypes.func,
getDhcpInterfaces: PropTypes.func,
t: PropTypes.func,
dhcp: PropTypes.object.isRequired,
toggleDhcp: PropTypes.func.isRequired,
getDhcpStatus: PropTypes.func.isRequired,
setDhcpConfig: PropTypes.func.isRequired,
findActiveDhcp: PropTypes.func.isRequired,
addStaticLease: PropTypes.func.isRequired,
removeStaticLease: PropTypes.func.isRequired,
toggleLeaseModal: PropTypes.func.isRequired,
getDhcpInterfaces: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
resetDhcp: PropTypes.func.isRequired,
};
export default withNamespaces()(Dhcp);

View File

@@ -0,0 +1,153 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import {
renderField,
renderRadioField,
renderSelectField,
required,
ipv4,
ipv6,
biggerOrEqualZero,
toNumber,
} from '../../../../helpers/form';
import { BLOCKING_MODES } from '../../../../helpers/constants';
const getFields = (processing, t) => Object.values(BLOCKING_MODES).map(mode => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
let Form = ({
handleSubmit, submitting, invalid, processing, blockingMode, t,
}) => (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
<Field
name="ratelimit"
type="number"
component={renderField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={[required, biggerOrEqualZero]}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="edns_cs_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('edns_enable')}
disabled={processing}
subtitle={t('edns_cs_desc')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans components={[<div key="0">text</div>]}>blocking_mode_desc</Trans>
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
</div>
</div>
</div>
{blockingMode === BLOCKING_MODES.custom_ip && (
<Fragment>
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="blocking_ipv4" className="form__label form__label--with-desc">
<Trans>blocking_ipv4</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv4_desc</Trans>
</div>
<Field
name="blocking_ipv4"
component={renderField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
/>
</div>
</div>
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ip_address" className="form__label form__label--with-desc">
<Trans>blocking_ipv6</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv6_desc</Trans>
</div>
<Field
name="blocking_ipv6"
component={renderField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv6, required]}
/>
</div>
</div>
</Fragment>
)}
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}
>
<Trans>save_btn</Trans>
</button>
</form>
);
Form.propTypes = {
blockingMode: PropTypes.string.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
const selector = formValueSelector('blockingModeForm');
Form = connect((state) => {
const blockingMode = selector(state, 'blocking_mode');
return {
blockingMode,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'blockingModeForm',
}),
])(Form);

View File

@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Card from '../../../ui/Card';
import Form from './Form';
const Config = ({ t, dnsConfig, setDnsConfig }) => {
const handleFormSubmit = (values) => {
setDnsConfig(values);
};
const {
blocking_mode,
ratelimit,
blocking_ipv4,
blocking_ipv6,
edns_cs_enabled,
processingSetConfig,
} = dnsConfig;
return (
<Card
title={t('dns_config')}
bodyType="card-body box-body--settings"
id="dns-config"
>
<div className="form">
<Form
initialValues={{
ratelimit,
blocking_mode,
blocking_ipv4,
blocking_ipv6,
edns_cs_enabled,
}}
onSubmit={handleFormSubmit}
processing={processingSetConfig}
/>
</div>
</Card>
);
};
Config.propTypes = {
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Config);

View File

@@ -67,7 +67,7 @@ class Table extends Component {
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
ofText="/"
rowsText={t('rows_table_footer_text')}
noDataText={t('rewrite_not_found')}
/>

View File

@@ -23,10 +23,10 @@ const Examples = props => (
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - {props.t('example_upstream_regular')}
<code>9.9.9.9</code> - {props.t('example_upstream_regular')}
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> &nbsp;
<code>tls://dns.quad9.net</code> &nbsp;
<span>
<Trans
components={[
@@ -45,7 +45,7 @@ const Examples = props => (
</span>
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> &nbsp;
<code>https://dns.quad9.net/dns-query</code> &nbsp;
<span>
<Trans
components={[
@@ -64,7 +64,7 @@ const Examples = props => (
</span>
</li>
<li>
<code>tcp://1.1.1.1</code> <Trans>example_upstream_tcp</Trans>
<code>tcp://9.9.9.9</code> <Trans>example_upstream_tcp</Trans>
</li>
<li>
<code>sdns://...</code> &nbsp;
@@ -102,7 +102,7 @@ const Examples = props => (
</span>
</li>
<li>
<code>[/example.local/]1.1.1.1</code> &nbsp;
<code>[/example.local/]9.9.9.9</code> &nbsp;
<span>
<Trans
components={[

View File

@@ -5,6 +5,7 @@ import { withNamespaces } from 'react-i18next';
import Upstream from './Upstream';
import Access from './Access';
import Rewrites from './Rewrites';
import Config from './Config';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
@@ -13,6 +14,7 @@ class Dns extends Component {
this.props.getDnsSettings();
this.props.getAccessList();
this.props.getRewritesList();
this.props.getDnsConfig();
}
render() {
@@ -29,12 +31,18 @@ class Dns extends Component {
addRewrite,
deleteRewrite,
toggleRewritesModal,
dnsConfig,
setDnsConfig,
} = this.props;
const isDataLoading =
dashboard.processingDnsSettings || access.processing || rewrites.processing;
const isDataReady =
!dashboard.processingDnsSettings && !access.processing && !rewrites.processing;
const isDataLoading = dashboard.processingDnsSettings
|| access.processing
|| rewrites.processing
|| dnsConfig.processingGetConfig;
const isDataReady = !dashboard.processingDnsSettings
&& !access.processing
&& !rewrites.processing
&& !dnsConfig.processingGetConfig;
return (
<Fragment>
@@ -42,6 +50,10 @@ class Dns extends Component {
{isDataLoading && <Loading />}
{isDataReady && (
<Fragment>
<Config
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<Upstream
upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
@@ -80,6 +92,9 @@ Dns.propTypes = {
deleteRewrite: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
getDnsSettings: PropTypes.func.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
getDnsConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};

View File

@@ -222,6 +222,7 @@ let Form = (props) => {
className="form-control mr-2"
value="path"
placeholder={t('encryption_certificates_source_path')}
disabled={!isEnabled}
/>
<Field
name="certificate_source"
@@ -230,6 +231,7 @@ let Form = (props) => {
className="form-control mr-2"
value="content"
placeholder={t('encryption_certificates_source_content')}
disabled={!isEnabled}
/>
</div>
</div>
@@ -289,6 +291,7 @@ let Form = (props) => {
className="form-control mr-2"
value="path"
placeholder={t('encryption_key_source_path')}
disabled={!isEnabled}
/>
<Field
name="key_source"
@@ -297,6 +300,7 @@ let Form = (props) => {
className="form-control mr-2"
value="content"
placeholder={t('encryption_key_source_content')}
disabled={!isEnabled}
/>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import Services from './Services';
import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig';
import FiltersConfig from './FiltersConfig';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';

View File

@@ -33,6 +33,36 @@
text-align: center;
}
.card-body--loading {
position: relative;
}
.card-body--loading:before {
content: "";
position: absolute;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.6);
}
.card-body--loading:after {
content: "";
position: absolute;
z-index: 101;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
margin-top: -20px;
margin-left: -20px;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E");
will-change: transform;
animation: clockwise 2s linear infinite;
}
.card-title-stats {
font-size: 13px;
color: #9aa0ac;

View File

@@ -3,3 +3,8 @@
vertical-align: middle;
height: 100%;
}
.icon--close {
width: 24px;
height: 24px;
}

View File

@@ -64,6 +64,14 @@ const Icons = () => (
<path d="M15 3C10.57 3 6.701 5.419 4.623 9h2.39a10.063 10.063 0 0 1 4.05-3.19c-.524.89-.961 1.973-1.3 3.19h2.108c.79-2.459 1.998-4 3.129-4s2.339 1.541 3.129 4h2.107c-.338-1.217-.774-2.3-1.299-3.19A10.062 10.062 0 0 1 22.989 9h2.389C23.298 5.419 19.43 3 15 3zm7.035 9.129c-1.372 0-2.264.73-2.264 1.842 0 .896.538 1.463 1.579 1.66l.75.15c.65.13.898.3.898.615 0 .375-.37.635-.91.635-.6 0-1.014-.265-1.049-.68h-1.38c.023 1.097.93 1.776 2.37 1.776 1.491 0 2.399-.717 2.399-1.904 0-.903-.504-1.412-1.63-1.63l-.734-.142c-.6-.118-.851-.3-.851-.611 0-.378.336-.62.844-.62.509 0 .891.28.923.682h1.336c-.024-1.053-.948-1.773-2.28-1.773zm-16.185.148v5.696h2.39c1.712 0 2.662-1.033 2.662-2.903 0-1.779-.966-2.793-2.662-2.793H5.85zm6.933.004v5.692h1.373v-3.235h.076l2.377 3.235h1.149V12.28h-1.373v3.203h-.076l-2.372-3.203h-1.154zm-5.486 1.16h.682c.912 0 1.449.596 1.449 1.657 0 1.128-.51 1.713-1.45 1.713h-.681v-3.37zM4.623 21C6.701 24.581 10.57 27 15 27c4.43 0 8.299-2.419 10.377-6h-2.389a10.063 10.063 0 0 1-4.049 3.19c.524-.89.96-1.973 1.297-3.19H18.13c-.79 2.459-1.996 4-3.127 4-1.131 0-2.339-1.541-3.129-4h-2.11c.339 1.217.776 2.3 1.3 3.19A10.056 10.056 0 0 1 7.013 21h-2.39z"></path>
</symbol>
<symbol id="service_amazon" viewBox="0 0 32 32" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M16.2,4c-3.3,0-6.9,1.2-7.7,5.3C8.4,9.7,8.7,10,9,10l3.3,0.3c0.3,0,0.6-0.3,0.6-0.6c0.3-1.4,1.5-2.1,2.8-2.1c0.7,0,1.5,0.3,1.9,0.9c0.5,0.7,0.4,1.7,0.4,2.5v0.5c-2,0.2-4.6,0.4-6.5,1.2c-2.2,0.9-3.7,2.8-3.7,5.7c0,3.6,2.3,5.4,5.2,5.4c2.5,0,3.8-0.6,5.7-2.5c0.6,0.9,0.9,1.4,2,2.3c0.3,0.1,0.6,0.1,0.8-0.1v0c0.7-0.6,2-1.7,2.7-2.3c0.3-0.2,0.2-0.6,0-0.9c-0.6-0.9-1.3-1.6-1.3-3.2v-5.4c0-2.3,0.2-4.4-1.5-6C20.1,4.4,17.9,4,16.2,4z M17.1,14.3c0.3,0,0.6,0,0.9,0v0.8c0,1.3,0.1,2.5-0.6,3.7c-0.5,1-1.4,1.6-2.4,1.6c-1.3,0-2.1-1-2.1-2.5C12.9,15.2,14.9,14.5,17.1,14.3z M26.7,22.4c-0.9,0-1.9,0.2-2.7,0.8c-0.2,0.2-0.2,0.4,0.1,0.4c0.9-0.1,2.8-0.4,3.2,0.1s-0.4,2.3-0.7,3.1c-0.1,0.2,0.1,0.3,0.3,0.2c1.5-1.2,1.9-3.8,1.6-4.2C28.3,22.5,27.6,22.4,26.7,22.4z M3.7,22.8c-0.2,0-0.3,0.3-0.1,0.4c3.3,3,7.6,4.7,12.4,4.7c3.4,0,7.4-1.1,10.2-3.1c0.5-0.3,0.1-0.9-0.4-0.7c-3.1,1.3-6.4,1.9-9.5,1.9c-4.5,0-8.8-1.2-12.4-3.3C3.8,22.9,3.7,22.8,3.7,22.8z" />
</symbol>
<symbol id="service_ebay" viewBox="0 0 128 128" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M95.633 48.416h31.826v55.802H95.633z"/><path d="M64 0C45.644 0 30.717 14.927 30.717 33.283h7.734C38.452 19.194 49.911 7.734 64 7.734s25.548 11.46 25.548 25.549h7.734C97.283 14.927 82.356 0 64 0zm63.459 33.283v15.133H.541V33.283h126.918zm0 70.935V128H.541v-23.782h126.918z"/><path d="M.541 48.416h31.826v55.802H.541z"/><path d="M32.367 48.416h31.194v55.802H32.367z"/><path d="M63.562 48.416h32.071v55.802H63.562z" />
</symbol>
<symbol id="service_youtube" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M19.695 4.04S15.348 3.2 12 3.2s-7.695.84-7.695.84L1.602 7.2v9.6l2.703 3.16s4.347.84 7.695.84 7.695-.84 7.695-.84l2.703-3.16V12 7.2zM9.602 15.68V8.32L16 12zm0 0"/><path d="M19.2 4a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.43 3.198-3.199C22.398 5.434 20.968 4 19.2 4zm0 9.602a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.434 3.198-3.2 0-1.769-1.43-3.198-3.199-3.198zM1.601 7.199c0 1.77 1.43 3.2 3.199 3.2 1.765 0 2.398-1.43 2.398-3.2C7.2 5.434 6.566 4 4.801 4 3.03 4 1.6 5.434 1.6 7.2zM4.8 13.602c-1.77 0-3.2 1.43-3.2 3.199A3.198 3.198 0 1 0 8 16.8c0-1.77-1.434-3.2-3.2-3.2zm0 0" />
</symbol>
@@ -104,6 +112,10 @@ const Icons = () => (
<path d="M83.5 72.814V512l17.432-2.865a955.35 955.35 0 0 1 88.604-10.312l13.965-.966V338.206L83.5 72.814z"/><path d="M308.5 0L308.5 172.328 428.5 438.914 428.5 0z"/><path d="M308.5 245.415l-10.87-24.149L198.03 0H83.501l168.12 371.813 57.024 126.112 8.852.566a955.65 955.65 0 0 1 93.572 10.644L428.5 512l-120-266.585z"/>
</symbol>
<symbol id="service_cloudflare" viewBox="0 0 50 50" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M38 10.813l-.906 3.78-1.907-3.405v1.718c2.899 2.301 4.926 5.79 5.126 9.688.699-.2 1.3-.188 2-.188 1.374 0 2.667.297 3.812.875l-1.031-.593 3.812-.875-3.812-.907L48.5 19h-3.813l2.813-2.688-3.688 1.094 2-3.312-3.312 2 1.094-3.688-2.688 2.781.094-3.874-2 3.28zM27 11c-5 0-9.414 2.992-11.313 7.594-.699-.399-1.687-.688-2.687-.688-3.2 0-5.906 2.606-5.906 5.907v.5c-3.899.3-7.094 3.68-7.094 7.78 0 .802.113 1.52.313 2.22.101.398.5.687 1 .687h47c.398 0 .675-.195.874-.594.5-1.101.813-2.207.813-3.406 0-4.2-3.488-7.594-7.688-7.594-.8 0-1.511.082-2.312.282l4.906 6.625-5.5-4.5L22 29.593l15.094-4.905L28.5 21.5l10.688 1.813v-.125C39.188 16.488 33.699 11 27 11zm19.781 12.656c.434.274.844.586 1.219.938h.5z" />
</symbol>
<symbol id="service_vk" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M12 .96C5.914.96.96 5.915.96 12c0 6.086 4.954 11.04 11.04 11.04 6.086 0 11.04-4.954 11.04-11.04C23.04 5.914 18.085.96 12 .96zm4.785 13.216c1.074.953 1.3 1.293 1.336 1.351.445.707-.492.793-.492.793h-1.98s-.481.004-.891-.27c-.672-.437-1.375-1.288-1.867-1.14-.414.125-.41.684-.41 1.16 0 .172-.149.25-.481.25h-.617c-1.086 0-2.262-.363-3.434-1.59-1.656-1.734-3.113-5.222-3.113-5.222s-.086-.176.008-.281c.105-.122.394-.106.394-.106h1.918s.18.031.309.125c.11.074.168.219.168.219s.32 1.062.734 1.742c.801 1.32 1.172 1.355 1.445 1.215.399-.207.266-1.617.266-1.617s.02-.602-.187-.871c-.16-.211-.465-.32-.598-.336-.11-.016.07-.203.3-.313.31-.137.727-.172 1.446-.164.563.004.723.04.941.09.665.152.5.555.5 1.969 0 .453-.062 1.09.278 1.3.148.09.652.204 1.55-1.257.43-.692.77-1.84.77-1.84s.067-.125.176-.188c.113-.066.11-.062.262-.062.152 0 1.683-.012 2.02-.012.335 0 .651-.004.702.191.078.282-.246 1.25-1.07 2.305-1.355 1.723-1.504 1.563-.383 2.559zm0 0" />
</symbol>
@@ -112,6 +124,10 @@ const Icons = () => (
<path d="M50 28c-3.313 0-6 2.688-6 6 0 3.313 2.688 6 6 6 3.313 0 6-2.688 6-6 0-3.313-2.688-6-6-6zm0 0" /><path d="M50 4C24.637 4 4 24.637 4 50s20.637 46 46 46 46-20.637 46-46S75.363 4 50 4zm0 16c7.73 0 14 6.27 14 14s-6.27 14-14 14-14-6.27-14-14 6.27-14 14-14zm14.828 49.172A3.999 3.999 0 0 1 62 76a3.987 3.987 0 0 1-2.828-1.172L50 65.656l-9.172 9.172a3.999 3.999 0 0 1-5.656 0 3.999 3.999 0 0 1 0-5.656l6.43-6.43c-1.836-.539-3.618-1.207-5.29-2.066A4.302 4.302 0 0 1 34 56.859c0-2.98 3.172-4.761 5.809-3.375A21.767 21.767 0 0 0 50 56c3.684 0 7.148-.91 10.191-2.516C62.828 52.098 66 53.88 66 56.86c0 1.602-.89 3.078-2.313 3.813-1.671.863-3.453 1.531-5.289 2.07zm0 0" />
</symbol>
<symbol id="service_origin" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M 12 4 C 11.539063 4 11.09375 4.046875 10.65625 4.121094 C 11.082031 3.183594 11.550781 2.445313 12 2 C 12.195313 1.804688 12.011719 1.484375 11.738281 1.539063 C 7.808594 2.359375 4 7.0625 4 12 C 4 16.417969 7.582031 20 12 20 C 12.460938 20 12.90625 19.953125 13.34375 19.878906 C 12.917969 20.816406 12.449219 21.554688 12 22 C 11.804688 22.195313 11.988281 22.515625 12.261719 22.460938 C 16.191406 21.640625 20 16.9375 20 12 C 20 7.582031 16.417969 4 12 4 Z M 12 15 C 10.34375 15 9 13.65625 9 12 C 9 10.34375 10.34375 9 12 9 C 13.65625 9 15 10.34375 15 12 C 15 13.65625 13.65625 15 12 15 Z" />
</symbol>
<symbol id="service_steam" viewBox="0 0 22 22" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M14.398 7.2a2.4 2.4 0 1 0 .003 4.799 2.4 2.4 0 0 0-.003-4.8zm0 0" fill="none" strokeWidth="1.6" stroke="currentColor" strokeMiterlimit="10"/><path d="M8 14c-.629 0-1.18.297-1.547.75l1.758.48c.426.114.68.555.562.98a.804.804 0 0 1-.984.563l-1.762-.48A1.998 1.998 0 0 0 10 16c0-1.105-.895-2-2-2zm0 0" /><path d="M19.2 3.2H4.8c-.886 0-1.6.714-1.6 1.6v9.063l2.027.551a3.213 3.213 0 0 1 2.289-1.566l2.136-2.567a4.799 4.799 0 1 1 4.066 4.066l-2.566 2.137A3.195 3.195 0 0 1 8 19.2 3.2 3.2 0 0 1 4.8 16c0-.016.005-.027.005-.043l-1.606-.437v3.68c0 .886.715 1.6 1.602 1.6h14.398c.887 0 1.602-.714 1.602-1.6V4.8c0-.886-.715-1.6-1.602-1.6zm0 0" />
</symbol>
@@ -120,6 +136,10 @@ const Icons = () => (
<path d="M 10 3 C 6.69 3 4 5.69 4 9 L 4 41.240234 L 25 47.539062 L 46 41.240234 L 46 9 C 46 5.69 43.31 3 40 3 L 10 3 z M 11 8 L 15 8 L 15 11 L 11 11 L 11 18 L 14 18 L 14 21 L 11 21 L 11 28 L 15 28 L 15 31 L 11 31 C 9.34 31 8 29.66 8 28 L 8 11 C 8 9.34 9.34 8 11 8 z M 17 8 L 23 8 C 24.66 8 26 9.34 26 11 L 26 18 C 26 19.66 24.66 21 23 21 L 20 21 L 20 31 L 17 31 L 17 8 z M 28 8 L 31 8 L 31 31 L 28 31 L 28 8 z M 36 8 L 39 8 C 40.66 8 42 9.34 42 11 L 42 15 L 39 15 L 39 11 L 36 11 L 36 28 L 39 28 L 39 24 L 42 24 L 42 28 C 42 29.66 40.66 31 39 31 L 36 31 C 34.34 31 33 29.66 33 28 L 33 11 C 33 9.34 34.34 8 36 8 z M 20 11 L 20 18 L 23 18 L 23 11 L 20 11 z M 9 34 L 13 34 C 13.55 34 14 34.45 14 35 L 14 36 L 13 36 L 13 35.25 C 13 35.11 12.89 35 12.75 35 L 9.25 35 C 9.11 35 9 35.11 9 35.25 L 9 38.75 C 9 38.89 9.11 39 9.25 39 L 12.75 39 C 12.89 39 13 38.89 13 38.75 L 13 38 L 12 38 L 12 37 L 14 37 L 14 39 C 14 39.55 13.55 40 13 40 L 9 40 C 8.45 40 8 39.55 8 39 L 8 35 C 8 34.45 8.45 34 9 34 z M 18 34 L 19 34 L 22 40 L 21 40 L 20.5 39 L 16.5 39 L 16 40 L 15 40 L 18 34 z M 23 34 L 24 34 L 26 38 L 28 34 L 29 34 L 29 40 L 28 40 L 28 36 L 26.5 39 L 25.5 39 L 24 36 L 24 40 L 23 40 L 23 34 z M 30 34 L 35 34 L 35 35 L 31 35 L 31 36.5 L 33 36.5 L 33 37.5 L 31 37.5 L 31 39 L 35 39 L 35 40 L 30 40 L 30 34 z M 37 34 L 41 34 C 41.55 34 42 34.45 42 35 L 42 35.5 L 41 35.5 L 41 35.25 C 41 35.11 40.89 35 40.75 35 L 37.25 35 C 37.11 35 37 35.11 37 35.25 L 37 36.25 C 37 36.39 37.11 36.5 37.25 36.5 L 41 36.5 C 41.55 36.5 42 36.95 42 37.5 L 42 39 C 42 39.55 41.55 40 41 40 L 37 40 C 36.45 40 36 39.55 36 39 L 36 38.5 L 37 38.5 L 37 38.75 C 37 38.89 37.11 39 37.25 39 L 40.75 39 C 40.89 39 41 38.89 41 38.75 L 41 37.75 C 41 37.61 40.89 37.5 40.75 37.5 L 37 37.5 C 36.45 37.5 36 37.05 36 36.5 L 36 35 C 36 34.45 36.45 34 37 34 z M 18.5 35 L 17 38 L 20 38 L 18.5 35 z"></path>
</symbol>
<symbol id="service_reddit" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M14.238 15.348c.085.084.085.221 0 .306-.465.462-1.194.687-2.231.687l-.008-.002-.008.002c-1.036 0-1.766-.225-2.231-.688-.085-.084-.085-.221 0-.305.084-.084.222-.084.307 0 .379.377 1.008.561 1.924.561l.008.002.008-.002c.915 0 1.544-.184 1.924-.561.085-.084.223-.084.307 0zm-3.44-2.418c0-.507-.414-.919-.922-.919-.509 0-.923.412-.923.919 0 .506.414.918.923.918.508.001.922-.411.922-.918zm13.202-.93c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12zm-5-.129c0-.851-.695-1.543-1.55-1.543-.417 0-.795.167-1.074.435-1.056-.695-2.485-1.137-4.066-1.194l.865-2.724 2.343.549-.003.034c0 .696.569 1.262 1.268 1.262.699 0 1.267-.566 1.267-1.262s-.568-1.262-1.267-1.262c-.537 0-.994.335-1.179.804l-2.525-.592c-.11-.027-.223.037-.257.145l-.965 3.038c-1.656.02-3.155.466-4.258 1.181-.277-.255-.644-.415-1.05-.415-.854.001-1.549.693-1.549 1.544 0 .566.311 1.056.768 1.325-.03.164-.05.331-.05.5 0 2.281 2.805 4.137 6.253 4.137s6.253-1.856 6.253-4.137c0-.16-.017-.317-.044-.472.486-.261.82-.766.82-1.353zm-4.872.141c-.509 0-.922.412-.922.919 0 .506.414.918.922.918s.922-.412.922-.918c0-.507-.413-.919-.922-.919z"/>
</symbol>
<symbol id="service_skype" viewBox="0 0 26 26" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M23.363 14.387c.153-.739.23-1.5.23-2.266C23.594 5.883 18.45.805 12.122.805c-.594 0-1.191.047-1.781.136A6.891 6.891 0 0 0 6.852 0C3.074 0 0 3.035 0 6.762c0 1.144.293 2.27.852 3.265-.133.688-.2 1.391-.2 2.094 0 6.238 5.149 11.316 11.47 11.316.648 0 1.3-.054 1.94-.164.95.477 2.012.727 3.086.727C20.926 24 24 20.969 24 17.238c0-1.004-.215-1.96-.637-2.851zM17.758 17.3c-.508.707-1.258 1.27-2.23 1.668-.966.394-2.122.593-3.434.593-1.578 0-2.903-.273-3.934-.812a5.074 5.074 0 0 1-1.808-1.582c-.47-.664-.707-1.324-.707-1.961 0-.395.156-.738.457-1.023.304-.278.687-.418 1.148-.418.379 0 .703.109.969.332.254.21.469.523.644.93.192.437.407.808.633 1.1.211.282.524.52.918.704.399.188.938.281 1.598.281.91 0 1.652-.191 2.215-.57.546-.367.812-.813.812-1.352 0-.43-.14-.765-.422-1.027-.3-.277-.699-.492-1.176-.637-.5-.152-1.18-.32-2.015-.496-1.14-.238-2.11-.523-2.88-.847-.788-.332-1.425-.79-1.89-1.364-.472-.582-.71-1.312-.71-2.172 0-.816.253-1.554.75-2.191.488-.633 1.206-1.125 2.132-1.46.91-.333 1.996-.5 3.223-.5.98 0 1.844.108 2.566.331.723.223 1.336.524 1.813.89.484.376.843.774 1.07 1.188.227.418.344.832.344 1.235 0 .386-.153.738-.453 1.046-.297.31-.68.465-1.125.465-.41 0-.727-.097-.95-.289-.207-.18-.418-.46-.656-.863-.273-.516-.605-.918-.984-1.203-.371-.277-.989-.418-1.836-.418-.79 0-1.43.156-1.902.465-.461.293-.684.633-.684 1.039 0 .246.07.449.219.629.156.187.379.351.656.488.289.145.586.258.883.34.308.082.82.207 1.523.367.887.191 1.707.398 2.43.625.73.234 1.363.516 1.879.848.527.34.941.773 1.238 1.293.297.52.445 1.16.445 1.91a4.07 4.07 0 0 1-.77 2.418zm0 0"/>
</symbol>
@@ -147,6 +167,14 @@ const Icons = () => (
<symbol id="location" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
<path d="M12,2C8.134,2,5,5.134,5,9c0,5,7,13,7,13s7-8,7-13C19,5.134,15.866,2,12,2z M12,11.5c-1.381,0-2.5-1.119-2.5-2.5 c0-1.381,1.119-2.5,2.5-2.5s2.5,1.119,2.5,2.5C14.5,10.381,13.381,11.5,12,11.5z"/>
</symbol>
<symbol id="cross" viewBox="0 0 24 24" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
</symbol>
<symbol id="plus" viewBox="0 0 24 24" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
</symbol>
</svg>
);

View File

@@ -1,23 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Card from '../ui/Card';
const Status = props => (
const Status = ({ message, buttonMessage, reloadPage }) => (
<div className="status">
<Card bodyType="card-body card-body--status">
<div className="h4 font-weight-light mb-4">
You are currently not using AdGuard Home
<Trans>{message}</Trans>
</div>
<button className="btn btn-success" onClick={props.handleStatusChange}>
Enable AdGuard Home
</button>
{buttonMessage &&
<button className="btn btn-success" onClick={reloadPage}>
<Trans>{buttonMessage}</Trans>
</button>}
</Card>
</div>
);
Status.propTypes = {
handleStatusChange: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
buttonMessage: PropTypes.string,
reloadPage: PropTypes.func,
};
export default Status;
export default withNamespaces()(Status);

View File

@@ -64,6 +64,7 @@
top: calc(100% + 10px);
right: -10px;
left: initial;
width: 255px;
transform: none;
}

View File

@@ -8,6 +8,7 @@ import {
toggleLeaseModal,
addStaticLease,
removeStaticLease,
resetDhcp,
} from '../actions';
import Dhcp from '../components/Settings/Dhcp';
@@ -28,6 +29,7 @@ const mapDispatchToProps = {
toggleLeaseModal,
addStaticLease,
removeStaticLease,
resetDhcp,
};
export default connect(

View File

@@ -7,17 +7,19 @@ import {
deleteRewrite,
toggleRewritesModal,
} from '../actions/rewrites';
import { getDnsConfig, setDnsConfig } from '../actions/dnsConfig';
import Dns from '../components/Settings/Dns';
const mapStateToProps = (state) => {
const {
dashboard, settings, access, rewrites,
dashboard, settings, access, rewrites, dnsConfig,
} = state;
const props = {
dashboard,
settings,
access,
rewrites,
dnsConfig,
};
return props;
};
@@ -33,6 +35,8 @@ const mapDispatchToProps = {
deleteRewrite,
toggleRewritesModal,
getDnsSettings,
getDnsConfig,
setDnsConfig,
};
export default connect(

View File

@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { addSuccessToast, getClients } from '../actions';
import { getFilteringStatus, setRules } from '../actions/filtering';
import { getLogs, getLogsConfig, setLogsPagination, setLogsFilter } from '../actions/queryLogs';
import { getLogs, getLogsConfig, setLogsPagination, setLogsFilter, setLogsPage } from '../actions/queryLogs';
import Logs from '../components/Logs';
const mapStateToProps = (state) => {
@@ -19,6 +19,7 @@ const mapDispatchToProps = {
getLogsConfig,
setLogsPagination,
setLogsFilter,
setLogsPage,
};
export default connect(

View File

@@ -118,6 +118,22 @@ export const LANGUAGES = [
key: 'zh-cn',
name: '简体中文',
},
{
key: 'no',
name: 'Norsk',
},
{
key: 'sr-cs',
name: 'Srpski',
},
{
key: 'hr',
name: 'Hrvatski',
},
{
key: 'fa',
name: 'فارسی',
},
{
key: 'ko',
name: '한국어',
@@ -141,6 +157,7 @@ export const STANDARD_HTTPS_PORT = 443;
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300;
export const DEBOUNCE_FILTER_TIMEOUT = 500;
export const CHECK_TIMEOUT = 1000;
export const STOP_TIMEOUT = 10000;
@@ -276,6 +293,22 @@ export const SERVICES = [
id: 'skype',
name: 'Skype',
},
{
id: 'amazon',
name: 'Amazon',
},
{
id: 'ebay',
name: 'eBay',
},
{
id: 'origin',
name: 'Origin',
},
{
id: 'cloudflare',
name: 'Cloudflare',
},
{
id: 'steam',
name: 'Steam',
@@ -284,6 +317,10 @@ export const SERVICES = [
id: 'epic_games',
name: 'Epic Games',
},
{
id: 'reddit',
name: 'Reddit',
},
{
id: 'ok',
name: 'OK',
@@ -320,6 +357,12 @@ export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
export const BLOCKING_MODES = {
nxdomain: 'nxdomain',
null_ip: 'null_ip',
custom_ip: 'custom_ip',
};
export const WHOIS_ICONS = {
location: 'location',
orgname: 'network',
@@ -379,3 +422,12 @@ export const DEFAULT_LOGS_FILTER = {
};
export const DEFAULT_LANGUAGE = 'en';
export const TABLE_DEFAULT_PAGE_SIZE = 100;
export const SMALL_TABLE_DEFAULT_PAGE_SIZE = 20;
export const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};

View File

@@ -29,6 +29,50 @@ export const renderField = ({
</Fragment>
);
export const renderGroupField = ({
input,
id,
className,
placeholder,
type,
disabled,
autoComplete,
isActionAvailable,
removeField,
meta: { touched, error },
}) => (
<Fragment>
<div className="input-group">
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
/>
{isActionAvailable &&
<span className="input-group-append">
<button
type="button"
className="btn btn-secondary btn-icon"
onClick={removeField}
>
<svg className="icon icon--close">
<use xlinkHref="#cross" />
</svg>
</button>
</span>
}
</div>
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const renderRadioField = ({
input, placeholder, disabled, meta: { touched, error },
}) => (
@@ -102,6 +146,7 @@ export const renderServiceField = ({
</Fragment>
);
// Validation functions
export const required = (value) => {
if (value || value === 0) {
return false;
@@ -111,6 +156,20 @@ export const required = (value) => {
export const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip4_format</Trans>;
}
return false;
};
export const ipv6 = (value) => {
if (value && !new RegExp(R_IPV6).test(value)) {
return <Trans>form_error_ip6_format</Trans>;
}
return false;
};
export const ip = (value) => {
if (value && !new RegExp(R_IPV4).test(value) && !new RegExp(R_IPV6).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
@@ -130,6 +189,13 @@ export const isPositive = (value) => {
return false;
};
export const biggerOrEqualZero = (value) => {
if (value < 0) {
return <Trans>form_error_negative</Trans>;
}
return false;
};
export const port = (value) => {
if ((value || value === 0) && (value < 80 || value > 65535)) {
return <Trans>form_error_port_range</Trans>;

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { getClientInfo, normalizeWhois } from './helpers';
import { normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants';
const getFormattedWhois = (whois, t) => {
@@ -22,26 +22,29 @@ const getFormattedWhois = (whois, t) => {
);
};
export const formatClientCell = (value, clients, autoClients, t) => {
const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value);
const { name, whois } = clientInfo;
export const formatClientCell = (row, t) => {
const { value, original: { info } } = row;
let whoisContainer = '';
let nameContainer = value;
if (name) {
nameContainer = (
<span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
{name} <small>({value})</small>
</span>
);
}
if (info) {
const { name, whois_info } = info;
if (whois) {
whoisContainer = (
<div className="logs__text logs__text--wrap logs__text--whois">
{getFormattedWhois(whois, t)}
</div>
);
if (name) {
nameContainer = (
<span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
{name} <small>({value})</small>
</span>
);
}
if (whois_info) {
whoisContainer = (
<div className="logs__text logs__text--wrap logs__text--whois">
{getFormattedWhois(whois_info, t)}
</div>
);
}
}
return (

View File

@@ -8,6 +8,7 @@ import subDays from 'date-fns/sub_days';
import round from 'lodash/round';
import axios from 'axios';
import i18n from 'i18next';
import uniqBy from 'lodash/uniqBy';
import versionCompare from './versionCompare';
import {
@@ -49,6 +50,7 @@ export const normalizeLogs = logs => logs.map((log) => {
rule,
service_name,
status,
original_answer,
} = log;
const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => {
@@ -64,8 +66,9 @@ export const normalizeLogs = logs => logs.map((log) => {
client,
filterId,
rule,
serviceName: service_name,
status,
serviceName: service_name,
originalAnswer: original_answer,
};
});
@@ -92,6 +95,17 @@ export const normalizeTopStats = stats => (
}))
);
export const addClientInfo = (data, clients, param) => (
data.map((row) => {
const clientIp = row[param];
const info = clients.find(item => item[clientIp]) || '';
return {
...row,
info: (info && info[clientIp]) || '',
};
})
);
export const normalizeFilteringStatus = (filteringStatus) => {
const {
enabled, filters, user_rules: userRules, interval,
@@ -248,6 +262,20 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
export const getClientInfo = (clients, ip) => {
const client = clients
.find(item => item.ip_addrs && item.ip_addrs.find(clientIp => clientIp === ip));
if (!client) {
return '';
}
const { name, whois_info } = client;
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
return { name, whois };
};
export const getAutoClientInfo = (clients, ip) => {
const client = clients.find(item => ip === item.ip);
if (!client) {
@@ -328,3 +356,13 @@ export const getPathWithQueryString = (path, params) => {
return `${path}?${searchParams.toString()}`;
};
export const getParamsForClientsSearch = (data, param) => {
const uniqueClients = uniqBy(data, param);
return uniqueClients
.reduce((acc, item, idx) => {
const key = `ip${idx}`;
acc[key] = item[param];
return acc;
}, {});
};

View File

@@ -22,12 +22,16 @@ import de from './__locales/de.json';
import id from './__locales/id.json';
import it from './__locales/it.json';
import ko from './__locales/ko.json';
import no from './__locales/no.json';
import nl from './__locales/nl.json';
import pl from './__locales/pl.json';
import ptPT from './__locales/pt-pt.json';
import sk from './__locales/sk.json';
import sl from './__locales/sl.json';
import tr from './__locales/tr.json';
import srCS from './__locales/sr-cs.json';
import hr from './__locales/hr.json';
import fa from './__locales/fa.json';
const resources = {
en: {
@@ -81,6 +85,9 @@ const resources = {
ko: {
translation: ko,
},
no: {
translation: no,
},
nl: {
translation: nl,
},
@@ -99,6 +106,15 @@ const resources = {
tr: {
translation: tr,
},
'sr-cs': {
translation: srCS,
},
hr: {
translation: hr,
},
fa: {
translation: fa,
},
};
const availableLanguages = Object.keys(resources);

View File

@@ -0,0 +1,50 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/dnsConfig';
import { BLOCKING_MODES } from '../helpers/constants';
const DEFAULT_BLOCKING_IPV4 = '0.0.0.0';
const DEFAULT_BLOCKING_IPV6 = '::';
const dnsConfig = handleActions(
{
[actions.getDnsConfigRequest]: state => ({ ...state, processingGetConfig: true }),
[actions.getDnsConfigFailure]: state =>
({ ...state, processingGetConfig: false }),
[actions.getDnsConfigSuccess]: (state, { payload }) => {
const {
blocking_ipv4,
blocking_ipv6,
...values
} = payload;
return {
...state,
...values,
blocking_ipv4: blocking_ipv4 || DEFAULT_BLOCKING_IPV4,
blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
processingGetConfig: false,
};
},
[actions.setDnsConfigRequest]: state => ({ ...state, processingSetConfig: true }),
[actions.setDnsConfigFailure]: state =>
({ ...state, processingSetConfig: false }),
[actions.setDnsConfigSuccess]: (state, { payload }) => ({
...state,
...payload,
processingSetConfig: false,
}),
},
{
processingGetConfig: false,
processingSetConfig: false,
blocking_mode: BLOCKING_MODES.nxdomain,
ratelimit: 20,
blocking_ipv4: DEFAULT_BLOCKING_IPV4,
blocking_ipv6: DEFAULT_BLOCKING_IPV6,
edns_cs_enabled: false,
},
);
export default dnsConfig;

View File

@@ -13,6 +13,7 @@ import rewrites from './rewrites';
import services from './services';
import stats from './stats';
import queryLogs from './queryLogs';
import dnsConfig from './dnsConfig';
import filtering from './filtering';
const settings = handleActions(
@@ -57,12 +58,13 @@ const settings = handleActions(
const dashboard = handleActions(
{
[actions.setDnsRunningStatus]: (state, { payload }) =>
({ ...state, isCoreRunning: payload }),
[actions.dnsStatusRequest]: state => ({ ...state, processing: true }),
[actions.dnsStatusFailure]: state => ({ ...state, processing: false }),
[actions.dnsStatusSuccess]: (state, { payload }) => {
const {
version,
running,
dns_port: dnsPort,
dns_addresses: dnsAddresses,
upstream_dns: upstreamDns,
@@ -74,7 +76,7 @@ const dashboard = handleActions(
} = payload;
const newState = {
...state,
isCoreRunning: running,
isCoreRunning: true,
processing: false,
dnsVersion: version,
dnsPort,
@@ -89,20 +91,6 @@ const dashboard = handleActions(
return newState;
},
[actions.enableDnsRequest]: state => ({ ...state, processing: true }),
[actions.enableDnsFailure]: state => ({ ...state, processing: false }),
[actions.enableDnsSuccess]: (state) => {
const newState = { ...state, isCoreRunning: !state.isCoreRunning, processing: false };
return newState;
},
[actions.disableDnsRequest]: state => ({ ...state, processing: true }),
[actions.disableDnsFailure]: state => ({ ...state, processing: false }),
[actions.disableDnsSuccess]: (state) => {
const newState = { ...state, isCoreRunning: !state.isCoreRunning, processing: false };
return newState;
},
[actions.getVersionRequest]: state => ({ ...state, processingVersion: true }),
[actions.getVersionFailure]: state => ({ ...state, processingVersion: false }),
[actions.getVersionSuccess]: (state, { payload }) => {
@@ -200,7 +188,7 @@ const dashboard = handleActions(
},
{
processing: true,
isCoreRunning: false,
isCoreRunning: true,
processingVersion: true,
processingFiltering: true,
processingClients: true,
@@ -289,6 +277,16 @@ const dhcp = handleActions(
return newState;
},
[actions.resetDhcpRequest]: state => ({ ...state, processingReset: true }),
[actions.resetDhcpFailure]: state => ({ ...state, processingReset: false }),
[actions.resetDhcpSuccess]: state => ({
...state,
processingReset: false,
config: {
enabled: false,
},
}),
[actions.toggleLeaseModal]: (state) => {
const newState = {
...state,
@@ -359,6 +357,7 @@ export default combineReducers({
rewrites,
services,
stats,
dnsConfig,
loadingBar: loadingBarReducer,
form: formReducer,
});

View File

@@ -20,25 +20,50 @@ const queryLogs = handleActions(
};
},
[actions.setLogsFilter]: (state, { payload }) => (
{ ...state, filter: payload }
),
[actions.setLogsPage]: (state, { payload }) => ({
...state,
page: payload,
}),
[actions.setLogsFilterRequest]: state => ({ ...state, processingGetLogs: true }),
[actions.setLogsFilterFailure]: state => ({ ...state, processingGetLogs: false }),
[actions.setLogsFilterSuccess]: (state, { payload }) => {
const { logs, oldest, filter } = payload;
const pageSize = 100;
const page = 0;
const pages = Math.ceil(logs.length / pageSize);
const total = logs.length;
const rowsStart = pageSize * page;
const rowsEnd = (pageSize * page) + pageSize;
const logsSlice = logs.slice(rowsStart, rowsEnd);
const isFiltered = Object.keys(filter).some(key => filter[key]);
return {
...state,
oldest,
filter,
isFiltered,
pages,
total,
logs: logsSlice,
allLogs: logs,
processingGetLogs: false,
};
},
[actions.getLogsRequest]: state => ({ ...state, processingGetLogs: true }),
[actions.getLogsFailure]: state => ({ ...state, processingGetLogs: false }),
[actions.getLogsSuccess]: (state, { payload }) => {
const {
logs, lastRowTime, page, pageSize, filtered,
logs, oldest, older_than, page, pageSize,
} = payload;
let logsWithOffset = state.allLogs.length > 0 ? state.allLogs : logs;
let allLogs = logs;
if (lastRowTime) {
if (older_than) {
logsWithOffset = [...state.allLogs, ...logs];
allLogs = [...state.allLogs, ...logs];
} else if (filtered) {
logsWithOffset = logs;
allLogs = logs;
}
const pages = Math.ceil(logsWithOffset.length / pageSize);
@@ -49,6 +74,7 @@ const queryLogs = handleActions(
return {
...state,
oldest,
pages,
total,
allLogs,
@@ -81,20 +107,33 @@ const queryLogs = handleActions(
...payload,
processingSetConfig: false,
}),
[actions.getAdditionalLogsRequest]: state => ({
...state, processingAdditionalLogs: true, processingGetLogs: true,
}),
[actions.getAdditionalLogsFailure]: state => ({
...state, processingAdditionalLogs: false, processingGetLogs: false,
}),
[actions.getAdditionalLogsSuccess]: state => ({
...state, processingAdditionalLogs: false, processingGetLogs: false,
}),
},
{
processingGetLogs: true,
processingClear: false,
processingGetConfig: false,
processingSetConfig: false,
processingAdditionalLogs: false,
logs: [],
interval: 1,
allLogs: [],
page: 0,
pages: 0,
total: 0,
enabled: true,
older_than: '',
oldest: '',
filter: DEFAULT_LOGS_FILTER,
isFiltered: false,
},
);

View File

@@ -36,6 +36,8 @@ func ipInRange(start, stop, ip net.IP) bool {
func (s *Server) dbLoad() {
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
dynLeases := []*Lease{}
staticLeases := []*Lease{}
data, err := ioutil.ReadFile(s.conf.DBFilePath)
if err != nil {
@@ -69,11 +71,47 @@ func (s *Server) dbLoad() {
Expiry: time.Unix(obj[i].Expiry, 0),
}
s.leases = append(s.leases, &lease)
if obj[i].Expiry == leaseExpireStatic {
staticLeases = append(staticLeases, &lease)
} else {
dynLeases = append(dynLeases, &lease)
}
}
s.leases = normalizeLeases(staticLeases, dynLeases)
for _, lease := range s.leases {
s.reserveIP(lease.IP, lease.HWAddr)
}
log.Info("DHCP: loaded %d leases from DB", numLeases)
log.Info("DHCP: loaded %d (%d) leases from DB", len(s.leases), numLeases)
}
// Skip duplicate leases
// Static leases have a priority over dynamic leases
func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
leases := []*Lease{}
index := map[string]int{}
for i, lease := range staticLeases {
_, ok := index[lease.HWAddr.String()]
if ok {
continue // skip the lease with the same HW address
}
index[lease.HWAddr.String()] = i
leases = append(leases, lease)
}
for i, lease := range dynLeases {
_, ok := index[lease.HWAddr.String()]
if ok {
continue // skip the lease with the same HW address
}
index[lease.HWAddr.String()] = i
leases = append(leases, lease)
}
return leases
}
// Store lease table in DB

485
dhcpd/dhcp_http.go Normal file
View File

@@ -0,0 +1,485 @@
package dhcpd
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
)
func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Info("DHCP: %s %s: %s", r.Method, r.URL, text)
http.Error(w, text, code)
}
// []Lease -> JSON
func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string {
leases := []map[string]string{}
for _, l := range inputLeases {
lease := map[string]string{
"mac": l.HWAddr.String(),
"ip": l.IP.String(),
"hostname": l.Hostname,
}
if includeExpires {
lease["expires"] = l.Expiry.Format(time.RFC3339)
}
leases = append(leases, lease)
}
return leases
}
func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
leases := convertLeases(s.Leases(), true)
staticLeases := convertLeases(s.StaticLeases(), false)
status := map[string]interface{}{
"config": s.conf,
"leases": leases,
"static_leases": staticLeases,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(status)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Unable to marshal DHCP status json: %s", err)
return
}
}
type staticLeaseJSON struct {
HWAddr string `json:"mac"`
IP string `json:"ip"`
Hostname string `json:"hostname"`
}
type dhcpServerConfigJSON struct {
ServerConfig `json:",inline"`
StaticLeases []staticLeaseJSON `json:"static_leases"`
}
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
newconfig := dhcpServerConfigJSON{}
err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
return
}
err = s.CheckConfig(newconfig.ServerConfig)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
}
err = s.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
err = s.Init(newconfig.ServerConfig)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
}
s.conf.ConfigModified()
if newconfig.Enabled {
staticIP, err := hasStaticIP(newconfig.InterfaceName)
if !staticIP && err == nil {
err = setStaticIP(newconfig.InterfaceName)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
return
}
}
err = s.Start()
if err != nil {
httpError(r, w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
}
}
}
type netInterface struct {
Name string `json:"name"`
MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"`
Addresses []string `json:"ip_addresses"`
Flags string `json:"flags"`
}
// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP
// invalid interface is a ppp interface or the one that doesn't allow broadcasts
func getValidNetInterfaces() ([]net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err)
}
netIfaces := []net.Interface{}
for i := range ifaces {
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
// this interface is ppp, we're not interested in this one
continue
}
iface := ifaces[i]
netIfaces = append(netIfaces, iface)
}
return netIfaces, nil
}
func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{}
ifaces, err := getValidNetInterfaces()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
}
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
// it's a loopback, skip it
continue
}
if iface.Flags&net.FlagBroadcast == 0 {
// this interface doesn't support broadcast, skip it
continue
}
addrs, err := iface.Addrs()
if err != nil {
httpError(r, 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 {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
httpError(r, 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(jsonIface.Addresses) != 0 {
response[iface.Name] = jsonIface
}
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Failed to marshal json with available interfaces: %s", err)
return
}
}
// Perform the following tasks:
// . Search for another DHCP server running
// . Check if a static IP is configured for the network interface
// Respond with results
func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
errorText := fmt.Sprintf("failed to read request body: %s", err)
log.Error(errorText)
http.Error(w, errorText, http.StatusBadRequest)
return
}
interfaceName := strings.TrimSpace(string(body))
if interfaceName == "" {
errorText := fmt.Sprintf("empty interface name specified")
log.Error(errorText)
http.Error(w, errorText, http.StatusBadRequest)
return
}
found, err := CheckIfOtherDHCPServersPresent(interfaceName)
othSrv := map[string]interface{}{}
foundVal := "no"
if found {
foundVal = "yes"
} else if err != nil {
foundVal = "error"
othSrv["error"] = err.Error()
}
othSrv["found"] = foundVal
staticIP := map[string]interface{}{}
isStaticIP, err := hasStaticIP(interfaceName)
staticIPStatus := "yes"
if err != nil {
staticIPStatus = "error"
staticIP["error"] = err.Error()
} else if !isStaticIP {
staticIPStatus = "no"
staticIP["ip"] = getFullIP(interfaceName)
}
staticIP["static"] = staticIPStatus
result := map[string]interface{}{}
result["other_server"] = othSrv
result["static_ip"] = staticIP
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Failed to marshal DHCP found json: %s", err)
return
}
}
// Check if network interface has a static IP configured
func hasStaticIP(ifaceName string) (bool, error) {
if runtime.GOOS == "windows" {
return false, errors.New("Can't detect static IP: not supported on Windows")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return false, err
}
lines := strings.Split(string(body), "\n")
nameLine := fmt.Sprintf("interface %s", ifaceName)
withinInterfaceCtx := false
for _, line := range lines {
line = strings.TrimSpace(line)
if withinInterfaceCtx && len(line) == 0 {
// an empty line resets our state
withinInterfaceCtx = false
}
if len(line) == 0 || line[0] == '#' {
continue
}
line = strings.TrimSpace(line)
if !withinInterfaceCtx {
if line == nameLine {
// we found our interface
withinInterfaceCtx = true
}
} else {
if strings.HasPrefix(line, "interface ") {
// we found another interface - reset our state
withinInterfaceCtx = false
continue
}
if strings.HasPrefix(line, "static ip_address=") {
return true, nil
}
}
}
return false, nil
}
// Get IP address with netmask
func getFullIP(ifaceName string) string {
cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName)
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
d, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return ""
}
fields := strings.Fields(string(d))
if len(fields) < 4 {
return ""
}
_, _, err = net.ParseCIDR(fields[3])
if err != nil {
return ""
}
return fields[3]
}
// Get gateway IP address
func getGatewayIP(ifaceName string) string {
cmd := exec.Command("ip", "route", "show", "dev", ifaceName)
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
d, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return ""
}
fields := strings.Fields(string(d))
if len(fields) < 3 || fields[0] != "default" {
return ""
}
ip := net.ParseIP(fields[2])
if ip == nil {
return ""
}
return fields[2]
}
// Set a static IP for network interface
func setStaticIP(ifaceName string) error {
ip := getFullIP(ifaceName)
if len(ip) == 0 {
return errors.New("Can't get IP address")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return err
}
ip4, _, err := net.ParseCIDR(ip)
if err != nil {
return err
}
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
ifaceName, ip)
body = append(body, []byte(add)...)
gatewayIP := getGatewayIP(ifaceName)
if len(gatewayIP) != 0 {
add = fmt.Sprintf("static routers=%s\n",
gatewayIP)
body = append(body, []byte(add)...)
}
add = fmt.Sprintf("static domain_name_servers=%s\n\n",
ip4)
body = append(body, []byte(add)...)
err = file.SafeWrite("/etc/dhcpcd.conf", body)
if err != nil {
return err
}
return nil
}
func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
lj := staticLeaseJSON{}
err := json.NewDecoder(r.Body).Decode(&lj)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
ip, _ := parseIPv4(lj.IP)
if ip == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP")
return
}
mac, _ := net.ParseMAC(lj.HWAddr)
lease := Lease{
IP: ip,
HWAddr: mac,
Hostname: lj.Hostname,
}
err = s.AddStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
}
func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
lj := staticLeaseJSON{}
err := json.NewDecoder(r.Body).Decode(&lj)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
ip, _ := parseIPv4(lj.IP)
if ip == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP")
return
}
mac, _ := net.ParseMAC(lj.HWAddr)
lease := Lease{
IP: ip,
HWAddr: mac,
Hostname: lj.Hostname,
}
err = s.RemoveStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
}
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
err := s.Stop()
if err != nil {
log.Error("DHCP: Stop: %s", err)
}
err = os.Remove(s.conf.DBFilePath)
if err != nil && !os.IsNotExist(err) {
log.Error("DHCP: os.Remove: %s: %s", s.conf.DBFilePath, err)
}
oldconf := s.conf
s.conf = ServerConfig{}
s.conf.LeaseDuration = 86400
s.conf.ICMPTimeout = 1000
s.conf.WorkDir = oldconf.WorkDir
s.conf.HTTPRegister = oldconf.HTTPRegister
s.conf.ConfigModified = oldconf.ConfigModified
s.conf.DBFilePath = oldconf.DBFilePath
s.conf.ConfigModified()
}
func (s *Server) registerHandlers() {
s.conf.HTTPRegister("GET", "/control/dhcp/status", s.handleDHCPStatus)
s.conf.HTTPRegister("GET", "/control/dhcp/interfaces", s.handleDHCPInterfaces)
s.conf.HTTPRegister("POST", "/control/dhcp/set_config", s.handleDHCPSetConfig)
s.conf.HTTPRegister("POST", "/control/dhcp/find_active_dhcp", s.handleDHCPFindActiveServer)
s.conf.HTTPRegister("POST", "/control/dhcp/add_static_lease", s.handleDHCPAddStaticLease)
s.conf.HTTPRegister("POST", "/control/dhcp/remove_static_lease", s.handleDHCPRemoveStaticLease)
s.conf.HTTPRegister("POST", "/control/dhcp/reset", s.handleReset)
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"sync"
@@ -38,13 +39,20 @@ type ServerConfig struct {
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
RangeStart string `json:"range_start" yaml:"range_start"`
RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds
WorkDir string `json:"-" yaml:"-"`
DBFilePath string `json:"-" yaml:"-"` // path to DB file
LeaseDuration uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
WorkDir string `json:"-" yaml:"-"`
DBFilePath string `json:"-" yaml:"-"` // path to DB file
// Called when the configuration is changed by HTTP request
ConfigModified func() `json:"-" yaml:"-"`
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `json:"-" yaml:"-"`
}
// Server - the current state of the DHCP server
@@ -88,6 +96,16 @@ func (s *Server) CheckConfig(config ServerConfig) error {
return tmpServer.setConfig(config)
}
// Create - create object
func Create(config ServerConfig) *Server {
s := Server{}
s.conf = config
if s.conf.HTTPRegister != nil {
s.registerHandlers()
}
return &s
}
// Init checks the configuration and initializes the server
func (s *Server) Init(config ServerConfig) error {
err := s.setConfig(config)
@@ -98,10 +116,12 @@ func (s *Server) Init(config ServerConfig) error {
return nil
}
func (s *Server) setConfig(config ServerConfig) error {
s.conf = config
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename)
// WriteDiskConfig - write configuration
func (s *Server) WriteDiskConfig(c *ServerConfig) {
*c = s.conf
}
func (s *Server) setConfig(config ServerConfig) error {
iface, err := net.InterfaceByName(config.InterfaceName)
if err != nil {
printInterfaces()
@@ -153,6 +173,12 @@ func (s *Server) setConfig(config ServerConfig) error {
dhcp4.OptionDomainNameServer: s.ipnet.IP,
}
oldconf := s.conf
s.conf = config
s.conf.WorkDir = oldconf.WorkDir
s.conf.HTTPRegister = oldconf.HTTPRegister
s.conf.ConfigModified = oldconf.ConfigModified
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename)
return nil
}
@@ -684,6 +710,21 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
return nil
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
now := time.Now().Unix()
s.leasesLock.RLock()
defer s.leasesLock.RUnlock()
for _, l := range s.leases {
if l.Expiry.Unix() > now && l.IP.Equal(ip) {
return l.HWAddr
}
}
return nil
}
// Reset internal state
func (s *Server) reset() {
s.leasesLock.Lock()

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/krolaw/dhcp4"
"github.com/stretchr/testify/assert"
)
func check(t *testing.T, result bool, msg string) {
@@ -211,3 +212,32 @@ func TestIsValidSubnetMask(t *testing.T) {
t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})")
}
}
func TestNormalizeLeases(t *testing.T) {
dynLeases := []*Lease{}
staticLeases := []*Lease{}
leases := []*Lease{}
lease := &Lease{}
lease.HWAddr = []byte{1, 2, 3, 4}
dynLeases = append(dynLeases, lease)
lease = new(Lease)
lease.HWAddr = []byte{1, 2, 3, 5}
dynLeases = append(dynLeases, lease)
lease = new(Lease)
lease.HWAddr = []byte{1, 2, 3, 4}
lease.IP = []byte{0, 2, 3, 4}
staticLeases = append(staticLeases, lease)
lease = new(Lease)
lease.HWAddr = []byte{2, 2, 3, 4}
staticLeases = append(staticLeases, lease)
leases = normalizeLeases(staticLeases, dynLeases)
assert.True(t, len(leases) == 3)
assert.True(t, bytes.Equal(leases[0].HWAddr, []byte{1, 2, 3, 4}))
assert.True(t, bytes.Equal(leases[0].IP, []byte{0, 2, 3, 4}))
assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4}))
assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5}))
}

View File

@@ -1,13 +1,7 @@
package dnsfilter
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/gob"
"encoding/json"
"fmt"
"io/ioutil"
"net"
@@ -16,34 +10,20 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/joomcode/errorx"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter"
"github.com/bluele/gcache"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"golang.org/x/net/publicsuffix"
)
const defaultHTTPTimeout = 5 * time.Minute
const defaultHTTPMaxIdleConnections = 100
const defaultSafebrowsingServer = "sb.adtidy.org"
const defaultSafebrowsingURL = "%s://%s/safebrowsing-lookup-hash.html?prefixes=%s"
const defaultParentalServer = "pctrl.adguard.com"
const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
const defaultParentalSensitivity = 13 // use "TEEN" by default
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
// ServiceEntry - blocked service array element
type ServiceEntry struct {
Name string
Rules []*urlfilter.NetworkRule
Rules []*rules.NetworkRule
}
// RequestFilteringSettings is custom filtering settings
@@ -65,7 +45,6 @@ type RewriteEntry struct {
type Config struct {
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
ParentalEnabled bool `yaml:"parental_enabled"`
UsePlainHTTP bool `yaml:"-"` // use plain HTTP for requests to parental and safe browsing servers
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
ResolverAddress string `yaml:"-"` // DNS server address
@@ -106,16 +85,14 @@ type filtersInitializerParams struct {
// Dnsfilter holds added rules and performs hostname matches against the rules
type Dnsfilter struct {
rulesStorage *urlfilter.RuleStorage
rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine
engineLock sync.RWMutex
// HTTP lookups for safebrowsing and parental
client http.Client // handle for http client -- single instance as recommended by docs
transport *http.Transport // handle for http transport used by http client
parentalServer string // access via methods
safeBrowsingServer string // access via methods
parentalServer string // access via methods
safeBrowsingServer string // access via methods
parentalUpstream upstream.Upstream
safeBrowsingUpstream upstream.Upstream
Config // for direct access by library users, even a = assignment
confLock sync.RWMutex
@@ -251,9 +228,6 @@ func (d *Dnsfilter) filtersInitializer() {
// Close - close the object
func (d *Dnsfilter) Close() {
if d != nil && d.transport != nil {
d.transport.CloseIdleConnections()
}
if d.rulesStorage != nil {
d.rulesStorage.Close()
}
@@ -261,7 +235,6 @@ func (d *Dnsfilter) Close() {
type dnsFilterContext struct {
stats Stats
dialCache gcache.Cache // "host" -> "IP" cache for safebrowsing and parental control servers
safebrowsingCache cache.Cache
parentalCache cache.Cache
safeSearchCache cache.Cache
@@ -297,10 +270,6 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
return Result{Reason: NotFilteredNotFound}, nil
}
host = strings.ToLower(host)
// prevent recursion
if host == d.parentalServer || host == d.safeBrowsingServer {
return Result{}, nil
}
var result Result
var err error
@@ -328,11 +297,10 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
}
}
// check safeSearch if no match
if setts.SafeSearchEnabled {
result, err = d.checkSafeSearch(host)
if err != nil {
log.Printf("Failed to safesearch HTTP lookup, ignoring check: %v", err)
log.Info("SafeSearch: failed: %v", err)
return Result{}, nil
}
@@ -341,12 +309,10 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
}
}
// check safebrowsing if no match
if setts.SafeBrowsingEnabled {
result, err = d.checkSafeBrowsing(host)
if err != nil {
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
log.Printf("Failed to do safebrowsing HTTP lookup, ignoring check: %v", err)
log.Info("SafeBrowsing: failed: %v", err)
return Result{}, nil
}
if result.Reason.Matched() {
@@ -354,12 +320,10 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
}
}
// check parental if no match
if setts.ParentalEnabled {
result, err = d.checkParental(host)
if err != nil {
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
log.Printf("Failed to do parental HTTP lookup, ignoring check: %v", err)
log.Printf("Parental: failed: %v", err)
return Result{}, nil
}
if result.Reason.Matched() {
@@ -367,7 +331,6 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
}
}
// nothing matched, return nothing
return Result{}, nil
}
@@ -426,7 +389,7 @@ func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result {
}
func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result {
req := urlfilter.NewRequestForHostname(host)
req := rules.NewRequestForHostname(host)
res := Result{}
for _, s := range svcs {
@@ -445,311 +408,6 @@ func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result {
return res
}
/*
expire byte[4]
res Result
*/
func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) {
var buf bytes.Buffer
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
var exp []byte
exp = make([]byte, 4)
binary.BigEndian.PutUint32(exp, uint32(expire))
_, _ = buf.Write(exp)
enc := gob.NewEncoder(&buf)
err := enc.Encode(res)
if err != nil {
log.Error("gob.Encode(): %s", err)
return
}
_ = cache.Set([]byte(host), buf.Bytes())
log.Debug("Stored in cache %p: %s", cache, host)
}
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
data := cache.Get([]byte(host))
if data == nil {
return Result{}, false
}
exp := int(binary.BigEndian.Uint32(data[:4]))
if exp <= int(time.Now().Unix()) {
cache.Del([]byte(host))
return Result{}, false
}
var buf bytes.Buffer
buf.Write(data[4:])
dec := gob.NewDecoder(&buf)
r := Result{}
err := dec.Decode(&r)
if err != nil {
log.Debug("gob.Decode(): %s", err)
return Result{}, false
}
return r, true
}
// for each dot, hash it and add it to string
func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
var hashparam bytes.Buffer
hashes := map[string]bool{}
tld, icann := publicsuffix.PublicSuffix(host)
if !icann {
// private suffixes like cloudfront.net
tld = ""
}
curhost := host
for {
if curhost == "" {
// we've reached end of string
break
}
if tld != "" && curhost == tld {
// we've reached the TLD, don't hash it
break
}
tohash := []byte(curhost)
if addslash {
tohash = append(tohash, '/')
}
sum := sha256.Sum256(tohash)
hexhash := fmt.Sprintf("%X", sum)
hashes[hexhash] = true
hashparam.WriteString(fmt.Sprintf("%02X%02X%02X%02X/", sum[0], sum[1], sum[2], sum[3]))
pos := strings.IndexByte(curhost, byte('.'))
if pos < 0 {
break
}
curhost = curhost[pos+1:]
}
return hashparam.String(), hashes
}
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeSearch HTTP lookup for %s", host)
}
// Check cache. Return cached result if it was found
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host)
if isFound {
atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
log.Tracef("%s: found in SafeSearch cache", host)
return cachedValue, nil
}
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
d.setCacheResult(gctx.safeSearchCache, host, res)
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
}
for _, i := range addrs {
if ipv4 := i.To4(); ipv4 != nil {
res.IP = ipv4
break
}
}
if len(res.IP) == 0 {
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
}
// Cache result
d.setCacheResult(gctx.safeSearchCache, host, res)
return res, nil
}
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeBrowsing HTTP lookup for %s", host)
}
format := func(hashparam string) string {
schema := "https"
if d.UsePlainHTTP {
schema = "http"
}
url := fmt.Sprintf(defaultSafebrowsingURL, schema, d.safeBrowsingServer, hashparam)
return url
}
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
result := Result{}
scanner := bufio.NewScanner(strings.NewReader(string(body)))
for scanner.Scan() {
line := scanner.Text()
splitted := strings.Split(line, ":")
if len(splitted) < 3 {
continue
}
hash := splitted[2]
if _, ok := hashes[hash]; ok {
// it's in the hash
result.IsFiltered = true
result.Reason = FilteredSafeBrowsing
result.Rule = splitted[0]
break
}
}
if err := scanner.Err(); err != nil {
// error, don't save cache
return Result{}, err
}
return result, nil
}
// check cache
cachedValue, isFound := getCachedResult(gctx.safebrowsingCache, host)
if isFound {
atomic.AddUint64(&gctx.stats.Safebrowsing.CacheHits, 1)
log.Tracef("%s: found in the lookup cache %p", host, gctx.safebrowsingCache)
return cachedValue, nil
}
result, err := d.lookupCommon(host, &gctx.stats.Safebrowsing, true, format, handleBody)
if err == nil {
d.setCacheResult(gctx.safebrowsingCache, host, result)
}
return result, err
}
func (d *Dnsfilter) checkParental(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("Parental HTTP lookup for %s", host)
}
format := func(hashparam string) string {
schema := "https"
if d.UsePlainHTTP {
schema = "http"
}
sensitivity := d.ParentalSensitivity
if sensitivity == 0 {
sensitivity = defaultParentalSensitivity
}
url := fmt.Sprintf(defaultParentalURL, schema, d.parentalServer, hashparam, sensitivity)
return url
}
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
// parse json
var m []struct {
Blocked bool `json:"blocked"`
ClientTTL int `json:"clientTtl"`
Reason string `json:"reason"`
Hash string `json:"hash"`
}
err := json.Unmarshal(body, &m)
if err != nil {
// error, don't save cache
log.Printf("Couldn't parse json '%s': %s", body, err)
return Result{}, err
}
result := Result{}
for i := range m {
if !hashes[m[i].Hash] {
continue
}
if m[i].Blocked {
result.IsFiltered = true
result.Reason = FilteredParental
result.Rule = fmt.Sprintf("parental %s", m[i].Reason)
break
}
}
return result, nil
}
// check cache
cachedValue, isFound := getCachedResult(gctx.parentalCache, host)
if isFound {
atomic.AddUint64(&gctx.stats.Parental.CacheHits, 1)
log.Tracef("%s: found in the lookup cache %p", host, gctx.parentalCache)
return cachedValue, nil
}
result, err := d.lookupCommon(host, &gctx.stats.Parental, false, format, handleBody)
if err == nil {
d.setCacheResult(gctx.parentalCache, host, result)
}
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, hashparamNeedSlash bool, format formatHandler, handleBody bodyHandler) (Result, error) {
// convert hostname to hash parameters
hashparam, hashes := hostnameToHashParam(host, hashparamNeedSlash)
// format URL with our hashes
url := format(hashparam)
// do HTTP request
atomic.AddUint64(&lookupstats.Requests, 1)
atomic.AddInt64(&lookupstats.Pending, 1)
updateMax(&lookupstats.Pending, &lookupstats.PendingMax)
resp, err := d.client.Get(url)
atomic.AddInt64(&lookupstats.Pending, -1)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
// error, don't save cache
return Result{}, err
}
// get body text
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// error, don't save cache
return Result{}, err
}
// handle status code
switch {
case resp.StatusCode == 204:
// empty result, save cache
return Result{}, nil
case resp.StatusCode != 200:
return Result{}, fmt.Errorf("HTTP status code: %d", resp.StatusCode)
}
result, err := handleBody(body, hashes)
if err != nil {
return Result{}, err
}
return result, nil
}
//
// Adding rule and matching against the rules
//
@@ -765,19 +423,19 @@ func fileExists(fn string) bool {
// Initialize urlfilter objects
func (d *Dnsfilter) initFiltering(filters map[int]string) error {
listArray := []urlfilter.RuleList{}
listArray := []filterlist.RuleList{}
for id, dataOrFilePath := range filters {
var list urlfilter.RuleList
var list filterlist.RuleList
if id == 0 {
list = &urlfilter.StringRuleList{
list = &filterlist.StringRuleList{
ID: 0,
RulesText: dataOrFilePath,
IgnoreCosmetic: true,
}
} else if !fileExists(dataOrFilePath) {
list = &urlfilter.StringRuleList{
list = &filterlist.StringRuleList{
ID: id,
IgnoreCosmetic: true,
}
@@ -789,7 +447,7 @@ func (d *Dnsfilter) initFiltering(filters map[int]string) error {
if err != nil {
return fmt.Errorf("ioutil.ReadFile(): %s: %s", dataOrFilePath, err)
}
list = &urlfilter.StringRuleList{
list = &filterlist.StringRuleList{
ID: id,
RulesText: string(data),
IgnoreCosmetic: true,
@@ -797,17 +455,17 @@ func (d *Dnsfilter) initFiltering(filters map[int]string) error {
} else {
var err error
list, err = urlfilter.NewFileRuleList(id, dataOrFilePath, true)
list, err = filterlist.NewFileRuleList(id, dataOrFilePath, true)
if err != nil {
return fmt.Errorf("urlfilter.NewFileRuleList(): %s: %s", dataOrFilePath, err)
return fmt.Errorf("filterlist.NewFileRuleList(): %s: %s", dataOrFilePath, err)
}
}
listArray = append(listArray, list)
}
rulesStorage, err := urlfilter.NewRuleStorage(listArray)
rulesStorage, err := filterlist.NewRuleStorage(listArray)
if err != nil {
return fmt.Errorf("urlfilter.NewRuleStorage(): %s", err)
return fmt.Errorf("filterlist.NewRuleStorage(): %s", err)
}
filteringEngine := urlfilter.NewDNSEngine(rulesStorage)
@@ -831,14 +489,14 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) {
return Result{}, nil
}
rules, ok := d.filteringEngine.Match(host)
frules, ok := d.filteringEngine.Match(host)
if !ok {
return Result{}, nil
}
log.Tracef("%d rules matched for host '%s'", len(rules), host)
log.Tracef("%d rules matched for host '%s'", len(frules), host)
for _, rule := range rules {
for _, rule := range frules {
log.Tracef("Found rule for host '%s': '%s' list_id: %d",
host, rule.Text(), rule.GetFilterListID())
@@ -849,7 +507,7 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) {
res.FilterID = int64(rule.GetFilterListID())
res.Rule = rule.Text()
if netRule, ok := rule.(*urlfilter.NetworkRule); ok {
if netRule, ok := rule.(*rules.NetworkRule); ok {
if netRule.Whitelist {
res.Reason = NotFilteredWhiteList
@@ -857,7 +515,7 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) {
}
return res, nil
} else if hostRule, ok := rule.(*urlfilter.HostRule); ok {
} else if hostRule, ok := rule.(*rules.HostRule); ok {
if qtype == dns.TypeA && hostRule.IP.To4() != nil {
// either IPv4 or IPv4-mapped IPv6 address
@@ -887,97 +545,6 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) {
return Result{}, nil
}
//
// lifecycle helper functions
//
// Return TRUE if this host's IP should be cached
func (d *Dnsfilter) shouldBeInDialCache(host string) bool {
return host == d.safeBrowsingServer ||
host == d.parentalServer
}
// Search for an IP address by host name
func searchInDialCache(host string) string {
rawValue, err := gctx.dialCache.Get(host)
if err != nil {
return ""
}
ip, _ := rawValue.(string)
log.Debug("Found in cache: %s -> %s", host, ip)
return ip
}
// Add "hostname" -> "IP address" entry to cache
func addToDialCache(host, ip string) {
err := gctx.dialCache.Set(host, ip)
if err != nil {
log.Debug("dialCache.Set: %s", err)
}
log.Debug("Added to cache: %s -> %s", host, ip)
}
type dialFunctionType func(ctx context.Context, network, addr string) (net.Conn, error)
// Connect to a remote server resolving hostname using our own DNS server
func (d *Dnsfilter) createCustomDialContext(resolverAddr string) dialFunctionType {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
log.Tracef("network:%v addr:%v", network, addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil {
con, err := dialer.DialContext(ctx, network, addr)
return con, err
}
cache := d.shouldBeInDialCache(host)
if cache {
ip := searchInDialCache(host)
if len(ip) != 0 {
addr = fmt.Sprintf("%s:%s", ip, port)
return dialer.DialContext(ctx, network, addr)
}
}
r := upstream.NewResolver(resolverAddr, 30*time.Second)
addrs, e := r.LookupIPAddr(ctx, host)
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
if e != nil {
return nil, e
}
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %s", host)
}
var dialErrs []error
for _, a := range addrs {
addr = fmt.Sprintf("%s:%s", a.String(), port)
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {
dialErrs = append(dialErrs, err)
continue
}
if cache {
addToDialCache(host, a.String())
}
return con, err
}
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
}
}
// New creates properly initialized DNS Filter that is ready to be used
func New(c *Config, filters map[int]string) *Dnsfilter {
@@ -1002,34 +569,16 @@ func New(c *Config, filters map[int]string) *Dnsfilter {
cacheConf.MaxSize = c.ParentalCacheSize
gctx.parentalCache = cache.New(cacheConf)
}
if len(c.ResolverAddress) != 0 && gctx.dialCache == nil {
dur := time.Duration(c.CacheTime) * time.Minute
gctx.dialCache = gcache.New(maxDialCacheSize).LRU().Expiration(dur).Build()
}
}
d := new(Dnsfilter)
// 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,
err := d.initSecurityServices()
if err != nil {
log.Error("dnsfilter: initialize services: %s", err)
return nil
}
if c != nil && len(c.ResolverAddress) != 0 {
d.transport.DialContext = d.createCustomDialContext(c.ResolverAddress)
}
d.client = http.Client{
Transport: d.transport,
Timeout: defaultHTTPTimeout,
}
d.safeBrowsingServer = defaultSafebrowsingServer
d.parentalServer = defaultParentalServer
if c != nil {
d.Config = *c
}
@@ -1053,38 +602,6 @@ func New(c *Config, filters map[int]string) *Dnsfilter {
return d
}
//
// config manipulation helpers
//
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
if len(host) == 0 {
d.safeBrowsingServer = defaultSafebrowsingServer
} else {
d.safeBrowsingServer = host
}
}
// SetHTTPTimeout lets you optionally change timeout during lookups
func (d *Dnsfilter) SetHTTPTimeout(t time.Duration) {
d.client.Timeout = t
}
// ResetHTTPTimeout resets lookup timeouts
func (d *Dnsfilter) ResetHTTPTimeout() {
d.client.Timeout = defaultHTTPTimeout
}
// SafeSearchDomain returns replacement address for search engine
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
if d.SafeSearchEnabled {
val, ok := safeSearchDomains[host]
return val, ok
}
return "", false
}
//
// stats
//

View File

@@ -3,15 +3,11 @@ package dnsfilter
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"path"
"runtime"
"testing"
"time"
"github.com/AdguardTeam/urlfilter"
"github.com/bluele/gcache"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
@@ -23,7 +19,6 @@ var setts RequestFilteringSettings
// SAFE SEARCH
// PARENTAL
// FILTERING
// CLIENTS SETTINGS
// BENCHMARKS
// HELPERS
@@ -126,34 +121,19 @@ func TestEtcHostsMatching(t *testing.T) {
// SAFE BROWSING
func TestSafeBrowsing(t *testing.T) {
testCases := []string{
"",
"sb.adtidy.org",
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s in %s", tc, _Func()), func(t *testing.T) {
d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
defer d.Close()
gctx.stats.Safebrowsing.Requests = 0
d.checkMatch(t, "wmconvirus.narod.ru")
d.checkMatch(t, "wmconvirus.narod.ru")
if gctx.stats.Safebrowsing.Requests != 1 {
t.Errorf("Safebrowsing lookup positive cache is not working: %v", gctx.stats.Safebrowsing.Requests)
}
d.checkMatch(t, "WMconvirus.narod.ru")
if gctx.stats.Safebrowsing.Requests != 1 {
t.Errorf("Safebrowsing lookup positive cache is not working: %v", gctx.stats.Safebrowsing.Requests)
}
d.checkMatch(t, "test.wmconvirus.narod.ru")
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "pornhub.com")
l := gctx.stats.Safebrowsing.Requests
d.checkMatchEmpty(t, "pornhub.com")
if gctx.stats.Safebrowsing.Requests != l {
t.Errorf("Safebrowsing lookup negative cache is not working: %v", gctx.stats.Safebrowsing.Requests)
}
})
}
d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
defer d.Close()
gctx.stats.Safebrowsing.Requests = 0
d.checkMatch(t, "wmconvirus.narod.ru")
d.checkMatch(t, "test.wmconvirus.narod.ru")
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "pornhub.com")
// test cached result
d.safeBrowsingServer = "127.0.0.1"
d.checkMatch(t, "wmconvirus.narod.ru")
d.checkMatchEmpty(t, "pornhub.com")
d.safeBrowsingServer = defaultSafebrowsingServer
}
func TestParallelSB(t *testing.T) {
@@ -172,33 +152,10 @@ func TestParallelSB(t *testing.T) {
})
}
// the only way to verify that custom server option is working is to point it at a server that does serve safebrowsing
func TestSafeBrowsingCustomServerFail(t *testing.T) {
d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
defer d.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Write("Hello, client")
fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()
address := ts.Listener.Addr().String()
d.SetHTTPTimeout(time.Second * 5)
d.SetSafeBrowsingServer(address) // this will ensure that test fails
d.checkMatchEmpty(t, "wmconvirus.narod.ru")
}
// SAFE SEARCH
func TestSafeSearch(t *testing.T) {
d := NewForTest(nil, nil)
defer d.Close()
_, ok := d.SafeSearchDomain("www.google.com")
if ok {
t.Errorf("Expected safesearch to error when disabled")
}
d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
d := NewForTest(&Config{SafeSearchEnabled: true}, nil)
defer d.Close()
val, ok := d.SafeSearchDomain("www.google.com")
if !ok {
@@ -355,24 +312,16 @@ func TestParentalControl(t *testing.T) {
defer d.Close()
d.ParentalSensitivity = 3
d.checkMatch(t, "pornhub.com")
d.checkMatch(t, "pornhub.com")
if gctx.stats.Parental.Requests != 1 {
t.Errorf("Parental lookup positive cache is not working")
}
d.checkMatch(t, "PORNhub.com")
if gctx.stats.Parental.Requests != 1 {
t.Errorf("Parental lookup positive cache is not working")
}
d.checkMatch(t, "www.pornhub.com")
d.checkMatchEmpty(t, "www.yandex.ru")
d.checkMatchEmpty(t, "yandex.ru")
l := gctx.stats.Parental.Requests
d.checkMatchEmpty(t, "yandex.ru")
if gctx.stats.Parental.Requests != l {
t.Errorf("Parental lookup negative cache is not working")
}
d.checkMatchEmpty(t, "api.jquery.com")
// test cached result
d.parentalServer = "127.0.0.1"
d.checkMatch(t, "pornhub.com")
d.checkMatchEmpty(t, "yandex.ru")
d.parentalServer = defaultParentalServer
}
// FILTERING
@@ -458,10 +407,10 @@ func applyClientSettings(setts *RequestFilteringSettings) {
setts.ParentalEnabled = false
setts.SafeBrowsingEnabled = true
rule, _ := urlfilter.NewNetworkRule("||facebook.com^", 0)
rule, _ := rules.NewNetworkRule("||facebook.com^", 0)
s := ServiceEntry{}
s.Name = "facebook"
s.Rules = []*urlfilter.NetworkRule{rule}
s.Rules = []*rules.NetworkRule{rule}
setts.ServicesRules = append(setts.ServicesRules, s)
}
@@ -588,17 +537,3 @@ func BenchmarkSafeSearchParallel(b *testing.B) {
}
})
}
func TestDnsfilterDialCache(t *testing.T) {
d := Dnsfilter{}
gctx.dialCache = gcache.New(1).LRU().Expiration(30 * time.Minute).Build()
d.shouldBeInDialCache("hostname")
if searchInDialCache("hostname") != "" {
t.Errorf("searchInDialCache")
}
addToDialCache("hostname", "1.1.1.1")
if searchInDialCache("hostname") != "1.1.1.1" {
t.Errorf("searchInDialCache")
}
}

View File

@@ -1,20 +0,0 @@
package dnsfilter
import (
"sync/atomic"
)
func updateMax(valuePtr *int64, maxPtr *int64) {
for {
current := atomic.LoadInt64(valuePtr)
max := atomic.LoadInt64(maxPtr)
if current <= max {
break
}
swapped := atomic.CompareAndSwapInt64(maxPtr, max, current)
if swapped {
break
}
// swapping failed because value has changed after reading, try again
}
}

View File

@@ -213,4 +213,6 @@ var safeSearchDomains = map[string]string{
"youtubei.googleapis.com": "restrictmoderate.youtube.com",
"youtube.googleapis.com": "restrictmoderate.youtube.com",
"www.youtube-nocookie.com": "restrictmoderate.youtube.com",
"pixabay.com": "safesearch.pixabay.com",
}

View File

@@ -4,17 +4,294 @@ package dnsfilter
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/gob"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
"golang.org/x/net/publicsuffix"
)
// Servers to use for resolution of SB/PC server name
var bootstrapServers = []string{"176.103.130.130", "176.103.130.131"}
const dnsTimeout = 3 * time.Second
const defaultSafebrowsingServer = "https://dns-family.adguard.com/dns-query"
const defaultParentalServer = "https://dns-family.adguard.com/dns-query"
const sbTXTSuffix = "sb.dns.adguard.com."
const pcTXTSuffix = "pc.dns.adguard.com."
func (d *Dnsfilter) initSecurityServices() error {
var err error
d.safeBrowsingServer = defaultSafebrowsingServer
d.parentalServer = defaultParentalServer
opts := upstream.Options{Timeout: dnsTimeout, Bootstrap: bootstrapServers}
d.parentalUpstream, err = upstream.AddressToUpstream(d.parentalServer, opts)
if err != nil {
return err
}
d.safeBrowsingUpstream, err = upstream.AddressToUpstream(d.safeBrowsingServer, opts)
if err != nil {
return err
}
return nil
}
/*
expire byte[4]
res Result
*/
func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) int {
var buf bytes.Buffer
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
var exp []byte
exp = make([]byte, 4)
binary.BigEndian.PutUint32(exp, uint32(expire))
_, _ = buf.Write(exp)
enc := gob.NewEncoder(&buf)
err := enc.Encode(res)
if err != nil {
log.Error("gob.Encode(): %s", err)
return 0
}
val := buf.Bytes()
_ = cache.Set([]byte(host), val)
return len(val)
}
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
data := cache.Get([]byte(host))
if data == nil {
return Result{}, false
}
exp := int(binary.BigEndian.Uint32(data[:4]))
if exp <= int(time.Now().Unix()) {
cache.Del([]byte(host))
return Result{}, false
}
var buf bytes.Buffer
buf.Write(data[4:])
dec := gob.NewDecoder(&buf)
r := Result{}
err := dec.Decode(&r)
if err != nil {
log.Debug("gob.Decode(): %s", err)
return Result{}, false
}
return r, true
}
// SafeSearchDomain returns replacement address for search engine
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
val, ok := safeSearchDomains[host]
return val, ok
}
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeSearch: lookup for %s", host)
}
// Check cache. Return cached result if it was found
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host)
if isFound {
// atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
log.Tracef("SafeSearch: found in cache: %s", host)
return cachedValue, nil
}
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
len := d.setCacheResult(gctx.safeSearchCache, host, res)
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, len)
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
}
for _, i := range addrs {
if ipv4 := i.To4(); ipv4 != nil {
res.IP = ipv4
break
}
}
if len(res.IP) == 0 {
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
}
// Cache result
len := d.setCacheResult(gctx.safeSearchCache, host, res)
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, len)
return res, nil
}
// for each dot, hash it and add it to string
func hostnameToHashParam(host string) (string, map[string]bool) {
var hashparam bytes.Buffer
hashes := map[string]bool{}
tld, icann := publicsuffix.PublicSuffix(host)
if !icann {
// private suffixes like cloudfront.net
tld = ""
}
curhost := host
for {
if curhost == "" {
// we've reached end of string
break
}
if tld != "" && curhost == tld {
// we've reached the TLD, don't hash it
break
}
sum := sha256.Sum256([]byte(curhost))
hashes[hex.EncodeToString(sum[:])] = true
hashparam.WriteString(fmt.Sprintf("%s.", hex.EncodeToString(sum[0:4])))
pos := strings.IndexByte(curhost, byte('.'))
if pos < 0 {
break
}
curhost = curhost[pos+1:]
}
return hashparam.String(), hashes
}
// Find the target hash in TXT response
func (d *Dnsfilter) processTXT(svc, host string, resp *dns.Msg, hashes map[string]bool) bool {
for _, a := range resp.Answer {
txt, ok := a.(*dns.TXT)
if !ok {
continue
}
log.Tracef("%s: hashes for %s: %v", svc, host, txt.Txt)
for _, t := range txt.Txt {
_, ok := hashes[t]
if ok {
log.Tracef("%s: matched %s by %s", svc, host, t)
return true
}
}
}
return false
}
// Disabling "dupl": the algorithm of SB/PC is similar, but it uses different data
// nolint:dupl
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeBrowsing lookup for %s", host)
}
// check cache
cachedValue, isFound := getCachedResult(gctx.safebrowsingCache, host)
if isFound {
// atomic.AddUint64(&gctx.stats.Safebrowsing.CacheHits, 1)
log.Tracef("SafeBrowsing: found in cache: %s", host)
return cachedValue, nil
}
result := Result{}
question, hashes := hostnameToHashParam(host)
question = question + sbTXTSuffix
log.Tracef("SafeBrowsing: checking %s: %s", host, question)
req := dns.Msg{}
req.SetQuestion(question, dns.TypeTXT)
resp, err := d.safeBrowsingUpstream.Exchange(&req)
if err != nil {
return result, err
}
if d.processTXT("SafeBrowsing", host, resp, hashes) {
result.IsFiltered = true
result.Reason = FilteredSafeBrowsing
result.Rule = "adguard-malware-shavar"
}
len := d.setCacheResult(gctx.safebrowsingCache, host, result)
log.Debug("SafeBrowsing: stored in cache: %s (%d bytes)", host, len)
return result, nil
}
// Disabling "dupl": the algorithm of SB/PC is similar, but it uses different data
// nolint:dupl
func (d *Dnsfilter) checkParental(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("Parental lookup for %s", host)
}
// check cache
cachedValue, isFound := getCachedResult(gctx.parentalCache, host)
if isFound {
// atomic.AddUint64(&gctx.stats.Parental.CacheHits, 1)
log.Tracef("Parental: found in cache: %s", host)
return cachedValue, nil
}
result := Result{}
question, hashes := hostnameToHashParam(host)
question = question + pcTXTSuffix
log.Tracef("Parental: checking %s: %s", host, question)
req := dns.Msg{}
req.SetQuestion(question, dns.TypeTXT)
resp, err := d.parentalUpstream.Exchange(&req)
if err != nil {
return result, err
}
if d.processTXT("Parental", host, resp, hashes) {
result.IsFiltered = true
result.Reason = FilteredParental
result.Rule = "parental CATEGORY_BLACKLISTED"
}
len := d.setCacheResult(gctx.parentalCache, host, result)
log.Debug("Parental: stored in cache: %s (%d bytes)", host, len)
return result, err
}
func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Info("DNSFilter: %s %s: %s", r.Method, r.URL, text)
@@ -170,9 +447,11 @@ func (d *Dnsfilter) registerSecurityHandlers() {
d.Config.HTTPRegister("POST", "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
d.Config.HTTPRegister("POST", "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
d.Config.HTTPRegister("GET", "/control/safebrowsing/status", d.handleSafeBrowsingStatus)
d.Config.HTTPRegister("POST", "/control/parental/enable", d.handleParentalEnable)
d.Config.HTTPRegister("POST", "/control/parental/disable", d.handleParentalDisable)
d.Config.HTTPRegister("GET", "/control/parental/status", d.handleParentalStatus)
d.Config.HTTPRegister("POST", "/control/safesearch/enable", d.handleSafeSearchEnable)
d.Config.HTTPRegister("POST", "/control/safesearch/disable", d.handleSafeSearchDisable)
d.Config.HTTPRegister("GET", "/control/safesearch/status", d.handleSafeSearchStatus)

188
dnsforward/access.go Normal file
View File

@@ -0,0 +1,188 @@
package dnsforward
import (
"encoding/json"
"net"
"net/http"
"sync"
"github.com/AdguardTeam/golibs/log"
)
type accessCtx struct {
lock sync.Mutex
allowedClients map[string]bool // IP addresses of whitelist clients
disallowedClients map[string]bool // IP addresses of clients that should be blocked
allowedClientsIPNet []net.IPNet // CIDRs of whitelist clients
disallowedClientsIPNet []net.IPNet // CIDRs of clients that should be blocked
blockedHosts map[string]bool // hosts that should be blocked
}
func (a *accessCtx) Init(allowedClients, disallowedClients, blockedHosts []string) error {
err := processIPCIDRArray(&a.allowedClients, &a.allowedClientsIPNet, allowedClients)
if err != nil {
return err
}
err = processIPCIDRArray(&a.disallowedClients, &a.disallowedClientsIPNet, disallowedClients)
if err != nil {
return err
}
convertArrayToMap(&a.blockedHosts, blockedHosts)
return nil
}
func convertArrayToMap(dst *map[string]bool, src []string) {
*dst = make(map[string]bool)
for _, s := range src {
(*dst)[s] = true
}
}
// Split array of IP or CIDR into 2 containers for fast search
func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []string) error {
*dst = make(map[string]bool)
for _, s := range src {
ip := net.ParseIP(s)
if ip != nil {
(*dst)[s] = true
continue
}
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
return err
}
*dstIPNet = append(*dstIPNet, *ipnet)
}
return nil
}
// IsBlockedIP - return TRUE if this client should be blocked
func (a *accessCtx) IsBlockedIP(ip string) bool {
a.lock.Lock()
defer a.lock.Unlock()
if len(a.allowedClients) != 0 || len(a.allowedClientsIPNet) != 0 {
_, ok := a.allowedClients[ip]
if ok {
return false
}
if len(a.allowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range a.allowedClientsIPNet {
if ipnet.Contains(ipAddr) {
return false
}
}
}
return true
}
_, ok := a.disallowedClients[ip]
if ok {
return true
}
if len(a.disallowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range a.disallowedClientsIPNet {
if ipnet.Contains(ipAddr) {
return true
}
}
}
return false
}
// IsBlockedDomain - return TRUE if this domain should be blocked
func (a *accessCtx) IsBlockedDomain(host string) bool {
a.lock.Lock()
_, ok := a.blockedHosts[host]
a.lock.Unlock()
return ok
}
type accessListJSON struct {
AllowedClients []string `json:"allowed_clients"`
DisallowedClients []string `json:"disallowed_clients"`
BlockedHosts []string `json:"blocked_hosts"`
}
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
s.RLock()
j := accessListJSON{
AllowedClients: s.conf.AllowedClients,
DisallowedClients: s.conf.DisallowedClients,
BlockedHosts: s.conf.BlockedHosts,
}
s.RUnlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(j)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
return
}
}
func checkIPCIDRArray(src []string) error {
for _, s := range src {
ip := net.ParseIP(s)
if ip != nil {
continue
}
_, _, err := net.ParseCIDR(s)
if err != nil {
return err
}
}
return nil
}
func (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {
j := accessListJSON{}
err := json.NewDecoder(r.Body).Decode(&j)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
err = checkIPCIDRArray(j.AllowedClients)
if err == nil {
err = checkIPCIDRArray(j.DisallowedClients)
}
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
a := &accessCtx{}
err = a.Init(j.AllowedClients, j.DisallowedClients, j.BlockedHosts)
if err != nil {
httpError(r, w, http.StatusBadRequest, "access.Init: %s", err)
return
}
s.Lock()
s.conf.AllowedClients = j.AllowedClients
s.conf.DisallowedClients = j.DisallowedClients
s.conf.BlockedHosts = j.BlockedHosts
s.access = a
s.Unlock()
s.conf.ConfigModified()
log.Debug("Access: updated lists: %d, %d, %d",
len(j.AllowedClients), len(j.DisallowedClients), len(j.BlockedHosts))
}

View File

@@ -2,7 +2,7 @@ package dnsforward
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"runtime"
@@ -28,6 +28,11 @@ const (
parentalBlockHost = "family-block.dns.adguard.com"
)
var defaultDNS = []string{
"https://dns.quad9.net/dns-query",
}
var defaultBootstrap = []string{"9.9.9.9", "149.112.112.112"}
// Server is the main way to start a DNS server.
//
// Example:
@@ -43,12 +48,14 @@ type Server struct {
dnsFilter *dnsfilter.Dnsfilter // DNS filter instance
queryLog querylog.QueryLog // Query log instance
stats stats.Stats
access *accessCtx
AllowedClients map[string]bool // IP addresses of whitelist clients
DisallowedClients map[string]bool // IP addresses of clients that should be blocked
AllowedClientsIPNet []net.IPNet // CIDRs of whitelist clients
DisallowedClientsIPNet []net.IPNet // CIDRs of clients that should be blocked
BlockedHosts map[string]bool // hosts that should be blocked
// DNS proxy instance for internal usage
// We don't Start() it and so no listen port is required.
internalProxy *proxy.Proxy
webRegistered bool
isRunning bool
sync.RWMutex
conf ServerConfig
@@ -61,37 +68,70 @@ func NewServer(dnsFilter *dnsfilter.Dnsfilter, stats stats.Stats, queryLog query
s.dnsFilter = dnsFilter
s.stats = stats
s.queryLog = queryLog
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
// Use plain DNS on MIPS, encryption is too slow
defaultDNS = defaultBootstrap
}
return s
}
// Close - close object
func (s *Server) Close() {
s.Lock()
s.dnsFilter = nil
s.stats = nil
s.queryLog = nil
s.dnsProxy = nil
s.Unlock()
}
func stringArrayDup(a []string) []string {
a2 := make([]string, len(a))
copy(a2, a)
return a2
}
// WriteDiskConfig - write configuration
func (s *Server) WriteDiskConfig(c *FilteringConfig) {
s.RLock()
sc := s.conf.FilteringConfig
*c = sc
c.RatelimitWhitelist = stringArrayDup(sc.RatelimitWhitelist)
c.BootstrapDNS = stringArrayDup(sc.BootstrapDNS)
c.AllowedClients = stringArrayDup(sc.AllowedClients)
c.DisallowedClients = stringArrayDup(sc.DisallowedClients)
c.BlockedHosts = stringArrayDup(sc.BlockedHosts)
c.UpstreamDNS = stringArrayDup(sc.UpstreamDNS)
s.RUnlock()
}
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct {
// Filtering callback function
FilterHandler func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"`
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
// This callback function returns the list of upstream servers for a client specified by IP address
GetUpstreamsByClient func(clientAddr string) []string `yaml:"-"`
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockingIPv4 string `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request
BlockingIPv6 string `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request
BlockingIPAddrv4 net.IP `yaml:"-"`
BlockingIPAddrv6 net.IP `yaml:"-"`
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
QueryLogInterval uint32 `yaml:"querylog_interval"` // time interval for query log (in days)
Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
Ratelimit uint32 `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
AllowedClients []string `yaml:"allowed_clients"` // IP addresses of whitelist clients
DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked
BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked
@@ -100,13 +140,8 @@ type FilteringConfig struct {
ParentalBlockHost string `yaml:"parental_block_host"`
SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"`
// Names of services to block (globally).
// Per-client settings can override this configuration.
BlockedServices []string `yaml:"blocked_services"`
CacheSize uint `yaml:"cache_size"` // DNS cache size (in bytes)
DnsfilterConf dnsfilter.Config `yaml:",inline"`
CacheSize uint `yaml:"cache_size"` // DNS cache size (in bytes)
UpstreamDNS []string `yaml:"upstream_dns"`
}
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
@@ -133,6 +168,12 @@ type ServerConfig struct {
FilteringConfig
TLSConfig
// Called when the configuration is changed by HTTP request
ConfigModified func()
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
}
// if any of ServerConfig values are zero, then default values from below are used
@@ -142,74 +183,96 @@ var defaultValues = ServerConfig{
FilteringConfig: FilteringConfig{BlockedResponseTTL: 3600},
}
func init() {
defaultDNS := []string{"8.8.8.8:53", "8.8.4.4:53"}
// Resolve - get IP addresses by host name from an upstream server.
// No request/response filtering is performed.
// Query log and Stats are not updated.
// This method may be called before Start().
func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
s.RLock()
defer s.RUnlock()
return s.internalProxy.LookupIPAddr(host)
}
defaultUpstreams := make([]upstream.Upstream, 0)
for _, addr := range defaultDNS {
u, err := upstream.AddressToUpstream(addr, upstream.Options{Timeout: DefaultTimeout})
if err == nil {
defaultUpstreams = append(defaultUpstreams, u)
}
// Exchange - send DNS request to an upstream server and receive response
// No request/response filtering is performed.
// Query log and Stats are not updated.
// This method may be called before Start().
func (s *Server) Exchange(req *dns.Msg) (*dns.Msg, error) {
s.RLock()
defer s.RUnlock()
ctx := &proxy.DNSContext{
Proto: "udp",
Req: req,
StartTime: time.Now(),
}
defaultValues.Upstreams = defaultUpstreams
err := s.internalProxy.Resolve(ctx)
if err != nil {
return nil, err
}
return ctx.Res, nil
}
// Start starts the DNS server
func (s *Server) Start(config *ServerConfig) error {
func (s *Server) Start() error {
s.Lock()
defer s.Unlock()
return s.startInternal(config)
}
func convertArrayToMap(dst *map[string]bool, src []string) {
*dst = make(map[string]bool)
for _, s := range src {
(*dst)[s] = true
}
}
// Split array of IP or CIDR into 2 containers for fast search
func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []string) error {
*dst = make(map[string]bool)
for _, s := range src {
ip := net.ParseIP(s)
if ip != nil {
(*dst)[s] = true
continue
}
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
return err
}
*dstIPNet = append(*dstIPNet, *ipnet)
}
return nil
return s.startInternal()
}
// startInternal starts without locking
func (s *Server) startInternal(config *ServerConfig) error {
if s.dnsProxy != nil {
return errors.New("DNS server is already started")
func (s *Server) startInternal() error {
err := s.dnsProxy.Start()
if err == nil {
s.isRunning = true
}
return err
}
// Prepare the object
func (s *Server) Prepare(config *ServerConfig) error {
if config != nil {
s.conf = *config
if s.conf.BlockingMode == "custom_ip" {
s.conf.BlockingIPAddrv4 = net.ParseIP(s.conf.BlockingIPv4)
s.conf.BlockingIPAddrv6 = net.ParseIP(s.conf.BlockingIPv6)
if s.conf.BlockingIPAddrv4 == nil || s.conf.BlockingIPAddrv6 == nil {
return fmt.Errorf("DNS: invalid custom blocking IP address specified")
}
}
}
if len(s.conf.UpstreamDNS) == 0 {
s.conf.UpstreamDNS = defaultDNS
}
if len(s.conf.BootstrapDNS) == 0 {
s.conf.BootstrapDNS = defaultBootstrap
}
upstreamConfig, err := proxy.ParseUpstreamsConfig(s.conf.UpstreamDNS, s.conf.BootstrapDNS, DefaultTimeout)
if err != nil {
return fmt.Errorf("DNS: proxy.ParseUpstreamsConfig: %s", err)
}
s.conf.Upstreams = upstreamConfig.Upstreams
s.conf.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
if len(s.conf.ParentalBlockHost) == 0 {
s.conf.ParentalBlockHost = parentalBlockHost
}
if len(s.conf.SafeBrowsingBlockHost) == 0 {
s.conf.SafeBrowsingBlockHost = safeBrowsingBlockHost
}
if s.conf.UDPListenAddr == nil {
s.conf.UDPListenAddr = defaultValues.UDPListenAddr
}
if s.conf.TCPListenAddr == nil {
s.conf.TCPListenAddr = defaultValues.TCPListenAddr
}
proxyConfig := proxy.Config{
UDPListenAddr: s.conf.UDPListenAddr,
TCPListenAddr: s.conf.TCPListenAddr,
Ratelimit: s.conf.Ratelimit,
Ratelimit: int(s.conf.Ratelimit),
RatelimitWhitelist: s.conf.RatelimitWhitelist,
RefuseAny: s.conf.RefuseAny,
CacheEnabled: true,
@@ -219,20 +282,23 @@ func (s *Server) startInternal(config *ServerConfig) error {
BeforeRequestHandler: s.beforeRequestHandler,
RequestHandler: s.handleDNSRequest,
AllServers: s.conf.AllServers,
EnableEDNSClientSubnet: s.conf.EnableEDNSClientSubnet,
}
err := processIPCIDRArray(&s.AllowedClients, &s.AllowedClientsIPNet, s.conf.AllowedClients)
intlProxyConfig := proxy.Config{
CacheEnabled: true,
CacheSizeBytes: 4096,
Upstreams: s.conf.Upstreams,
DomainsReservedUpstreams: s.conf.DomainsReservedUpstreams,
}
s.internalProxy = &proxy.Proxy{Config: intlProxyConfig}
s.access = &accessCtx{}
err = s.access.Init(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
if err != nil {
return err
}
err = processIPCIDRArray(&s.DisallowedClients, &s.DisallowedClientsIPNet, s.conf.DisallowedClients)
if err != nil {
return err
}
convertArrayToMap(&s.BlockedHosts, s.conf.BlockedHosts)
if s.conf.TLSListenAddr != nil && len(s.conf.CertificateChainData) != 0 && len(s.conf.PrivateKeyData) != 0 {
proxyConfig.TLSListenAddr = s.conf.TLSListenAddr
keypair, err := tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
@@ -245,21 +311,18 @@ func (s *Server) startInternal(config *ServerConfig) error {
}
}
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
log.Fatal("len(proxyConfig.Upstreams) == 0")
}
if !s.webRegistered && s.conf.HTTPRegister != nil {
s.webRegistered = true
s.registerHandlers()
}
// Initialize and start the DNS proxy
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
return s.dnsProxy.Start()
return nil
}
// Stop stops the DNS server
@@ -273,24 +336,37 @@ func (s *Server) Stop() error {
func (s *Server) stopInternal() error {
if s.dnsProxy != nil {
err := s.dnsProxy.Stop()
s.dnsProxy = nil
if err != nil {
return errorx.Decorate(err, "could not stop the DNS server properly")
}
}
s.isRunning = false
return nil
}
// IsRunning returns true if the DNS server is running
func (s *Server) IsRunning() bool {
s.RLock()
isRunning := true
if s.dnsProxy == nil {
isRunning = false
defer s.RUnlock()
return s.isRunning
}
// Restart - restart server
func (s *Server) Restart() error {
s.Lock()
defer s.Unlock()
log.Print("Start reconfiguring the server")
err := s.stopInternal()
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
s.RUnlock()
return isRunning
err = s.startInternal()
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
return nil
}
// Reconfigure applies the new configuration to the DNS server
@@ -304,12 +380,16 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
return errorx.Decorate(err, "could not reconfigure the server")
}
// On some Windows versions the UDP port we've just closed in proxy.Stop() doesn't get actually closed right away.
if runtime.GOOS == "windows" {
time.Sleep(1 * time.Second)
// It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed.
time.Sleep(100 * time.Millisecond)
err = s.Prepare(config)
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
err = s.startInternal(config)
err = s.startInternal()
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
@@ -320,63 +400,36 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
// 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)
p := s.dnsProxy
s.RUnlock()
if p != nil { // an attempt to protect against race in case we're here after Close() was called
p.ServeHTTP(w, r)
}
}
// Return TRUE if this client should be blocked
func (s *Server) isBlockedIP(ip string) bool {
if len(s.AllowedClients) != 0 || len(s.AllowedClientsIPNet) != 0 {
_, ok := s.AllowedClients[ip]
if ok {
return false
}
if len(s.AllowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range s.AllowedClientsIPNet {
if ipnet.Contains(ipAddr) {
return false
}
}
}
return true
// Get IP address from net.Addr object
// Note: we can't use net.SplitHostPort(a.String()) because of IPv6 zone:
// https://github.com/AdguardTeam/AdGuardHome/issues/1261
func ipFromAddr(a net.Addr) string {
switch addr := a.(type) {
case *net.UDPAddr:
return addr.IP.String()
case *net.TCPAddr:
return addr.IP.String()
}
_, ok := s.DisallowedClients[ip]
if ok {
return true
}
if len(s.DisallowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range s.DisallowedClientsIPNet {
if ipnet.Contains(ipAddr) {
return true
}
}
}
return false
}
// Return TRUE if this domain should be blocked
func (s *Server) isBlockedDomain(host string) bool {
_, ok := s.BlockedHosts[host]
return ok
return ""
}
func (s *Server) beforeRequestHandler(p *proxy.Proxy, d *proxy.DNSContext) (bool, error) {
ip, _, _ := net.SplitHostPort(d.Addr.String())
if s.isBlockedIP(ip) {
ip := ipFromAddr(d.Addr)
if s.access.IsBlockedIP(ip) {
log.Tracef("Client IP %s is blocked by settings", ip)
return false, nil
}
if len(d.Req.Question) == 1 {
host := strings.TrimSuffix(d.Req.Question[0].Name, ".")
if s.isBlockedDomain(host) {
if s.access.IsBlockedDomain(host) {
log.Tracef("Domain %s is blocked by settings", host)
return false, nil
}
@@ -386,6 +439,7 @@ func (s *Server) beforeRequestHandler(p *proxy.Proxy, d *proxy.DNSContext) (bool
}
// handleDNSRequest filters the incoming DNS requests and writes them to the query log
// nolint (gocyclo)
func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
start := time.Now()
@@ -414,6 +468,7 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
return err
}
var origResp *dns.Msg
if d.Res == nil {
answer := []dns.RR{}
originalQuestion := d.Req.Question[0]
@@ -424,6 +479,19 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
}
if d.Addr != nil && s.conf.GetUpstreamsByClient != nil {
clientIP := ipFromAddr(d.Addr)
upstreams := s.conf.GetUpstreamsByClient(clientIP)
for _, us := range upstreams {
u, err := upstream.AddressToUpstream(us, upstream.Options{Timeout: 30 * time.Second})
if err != nil {
log.Error("upstream.AddressToUpstream: %s: %s", us, err)
continue
}
d.Upstreams = append(d.Upstreams, u)
}
}
// request was not filtered so let it be processed further
err = p.Resolve(d)
if err != nil {
@@ -432,11 +500,24 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
d.Req.Question[0] = originalQuestion
d.Res.Question[0] = originalQuestion
if len(d.Res.Answer) != 0 {
answer = append(answer, d.Res.Answer...) // host -> IP
d.Res.Answer = answer
}
} else if res.Reason != dnsfilter.NotFilteredWhiteList {
origResp2 := d.Res
res, err = s.filterResponse(d)
if err != nil {
return err
}
if res != nil {
origResp = origResp2 // matched by response
} else {
res = &dnsfilter.Result{}
}
}
}
@@ -457,11 +538,18 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
// Synchronize access to s.queryLog and s.stats so they won't be suddenly uninitialized while in use.
// This can happen after proxy server has been stopped, but its workers haven't yet exited.
if shouldLog && s.queryLog != nil {
upstreamAddr := ""
if d.Upstream != nil {
upstreamAddr = d.Upstream.Address()
p := querylog.AddParams{
Question: msg,
Answer: d.Res,
OrigAnswer: origResp,
Result: res,
Elapsed: elapsed,
ClientIP: getIP(d.Addr),
}
s.queryLog.Add(msg, d.Res, res, elapsed, d.Addr, upstreamAddr)
if d.Upstream != nil {
p.Upstream = d.Upstream.Address()
}
s.queryLog.Add(p)
}
s.updateStats(d, elapsed, *res)
@@ -470,6 +558,17 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
return nil
}
// Get IP address from net.Addr
func getIP(addr net.Addr) net.IP {
switch addr := addr.(type) {
case *net.UDPAddr:
return addr.IP
case *net.TCPAddr:
return addr.IP
}
return nil
}
func (s *Server) updateStats(d *proxy.DNSContext, elapsed time.Duration, res dnsfilter.Result) {
if s.stats == nil {
return
@@ -517,14 +616,10 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
return &dnsfilter.Result{}, nil
}
clientAddr := ""
if d.Addr != nil {
clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
}
setts := s.dnsFilter.GetConfig()
setts.FilteringEnabled = true
if s.conf.FilterHandler != nil {
clientAddr := ipFromAddr(d.Addr)
s.conf.FilterHandler(clientAddr, &setts)
}
@@ -540,8 +635,7 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
d.Res = s.genDNSFilterMessage(d, &res)
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.IPList) != 0 {
resp := dns.Msg{}
resp.SetReply(req)
resp := s.makeResponse(req)
name := host
if len(res.CanonName) != 0 {
@@ -562,12 +656,69 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
}
}
d.Res = &resp
d.Res = resp
}
return &res, err
}
// If response contains CNAME, A or AAAA records, we apply filtering to each canonical host name or IP address.
// If this is a match, we set a new response in d.Res and return.
func (s *Server) filterResponse(d *proxy.DNSContext) (*dnsfilter.Result, error) {
for _, a := range d.Res.Answer {
host := ""
switch v := a.(type) {
case *dns.CNAME:
log.Debug("DNSFwd: Checking CNAME %s for %s", v.Target, v.Hdr.Name)
host = strings.TrimSuffix(v.Target, ".")
case *dns.A:
host = v.A.String()
log.Debug("DNSFwd: Checking record A (%s) for %s", host, v.Hdr.Name)
case *dns.AAAA:
host = v.AAAA.String()
log.Debug("DNSFwd: Checking record AAAA (%s) for %s", host, v.Hdr.Name)
default:
continue
}
s.RLock()
// Synchronize access to s.dnsFilter so it won't be suddenly uninitialized while in use.
// This could happen after proxy server has been stopped, but its workers are not yet exited.
if !s.conf.ProtectionEnabled || s.dnsFilter == nil {
s.RUnlock()
continue
}
setts := dnsfilter.RequestFilteringSettings{}
setts.FilteringEnabled = true
res, err := s.dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts)
s.RUnlock()
if err != nil {
return nil, err
} else if res.IsFiltered {
d.Res = s.genDNSFilterMessage(d, &res)
log.Debug("DNSFwd: Matched %s by response: %s", d.Req.Question[0].Name, host)
return &res, nil
}
}
return nil, nil
}
// Create a DNS response by DNS request and set necessary flags
func (s *Server) makeResponse(req *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(req)
resp.RecursionAvailable = true
resp.Compress = true
return &resp
}
// genDNSFilterMessage generates a DNS message corresponding to the filtering result
func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Result) *dns.Msg {
m := d.Req
@@ -593,6 +744,14 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
case dns.TypeAAAA:
return s.genAAAARecord(m, net.IPv6zero)
}
} else if s.conf.BlockingMode == "custom_ip" {
switch m.Question[0].Qtype {
case dns.TypeA:
return s.genARecord(m, s.conf.BlockingIPAddrv4)
case dns.TypeAAAA:
return s.genAAAARecord(m, s.conf.BlockingIPAddrv6)
}
}
return s.genNXDomain(m)
@@ -607,17 +766,15 @@ func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
}
func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(request)
resp := s.makeResponse(request)
resp.Answer = append(resp.Answer, s.genAAnswer(request, ip))
return &resp
return resp
}
func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(request)
resp := s.makeResponse(request)
resp.Answer = append(resp.Answer, s.genAAAAAnswer(request, ip))
return &resp
return resp
}
func (s *Server) genAAnswer(req *dns.Msg, ip net.IP) *dns.A {
@@ -653,9 +810,8 @@ func (s *Server) genResponseWithIP(req *dns.Msg, ip net.IP) *dns.Msg {
}
// empty response
resp := dns.Msg{}
resp.SetReply(req)
return &resp
resp := s.makeResponse(req)
return resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
@@ -683,9 +839,7 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
return s.genServerFailure(request)
}
resp := dns.Msg{}
resp.SetReply(request)
resp.Authoritative, resp.RecursionAvailable = true, true
resp := s.makeResponse(request)
if newContext.Res != nil {
for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
@@ -693,7 +847,7 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
}
}
return &resp
return resp
}
// Make a CNAME response

View File

@@ -0,0 +1,381 @@
package dnsforward
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/jsonutil"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils"
"github.com/miekg/dns"
)
func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Info("DNS: %s %s: %s", r.Method, r.URL, text)
http.Error(w, text, code)
}
type dnsConfigJSON struct {
ProtectionEnabled bool `json:"protection_enabled"`
RateLimit uint32 `json:"ratelimit"`
BlockingMode string `json:"blocking_mode"`
BlockingIPv4 string `json:"blocking_ipv4"`
BlockingIPv6 string `json:"blocking_ipv6"`
EDNSCSEnabled bool `json:"edns_cs_enabled"`
}
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
resp := dnsConfigJSON{}
s.RLock()
resp.ProtectionEnabled = s.conf.ProtectionEnabled
resp.BlockingMode = s.conf.BlockingMode
resp.BlockingIPv4 = s.conf.BlockingIPv4
resp.BlockingIPv6 = s.conf.BlockingIPv6
resp.RateLimit = s.conf.Ratelimit
resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
s.RUnlock()
js, err := json.Marshal(resp)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
func checkBlockingMode(req dnsConfigJSON) bool {
bm := req.BlockingMode
if !(bm == "nxdomain" || bm == "null_ip" || bm == "custom_ip") {
return false
}
if bm == "custom_ip" {
ip := net.ParseIP(req.BlockingIPv4)
if ip == nil || ip.To4() == nil {
return false
}
ip = net.ParseIP(req.BlockingIPv6)
if ip == nil {
return false
}
}
return true
}
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := dnsConfigJSON{}
js, err := jsonutil.DecodeObject(&req, r.Body)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
if js.Exists("blocking_mode") && !checkBlockingMode(req) {
httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
return
}
restart := false
s.Lock()
if js.Exists("protection_enabled") {
s.conf.ProtectionEnabled = req.ProtectionEnabled
}
if js.Exists("blocking_mode") {
s.conf.BlockingMode = req.BlockingMode
if req.BlockingMode == "custom_ip" {
if js.Exists("blocking_ipv4") {
s.conf.BlockingIPv4 = req.BlockingIPv4
s.conf.BlockingIPAddrv4 = net.ParseIP(req.BlockingIPv4)
}
if js.Exists("blocking_ipv6") {
s.conf.BlockingIPv6 = req.BlockingIPv6
s.conf.BlockingIPAddrv6 = net.ParseIP(req.BlockingIPv6)
}
}
}
if js.Exists("ratelimit") {
if s.conf.Ratelimit != req.RateLimit {
restart = true
}
s.conf.Ratelimit = req.RateLimit
}
if js.Exists("edns_cs_enabled") {
s.conf.EnableEDNSClientSubnet = req.EDNSCSEnabled
restart = true
}
s.Unlock()
s.conf.ConfigModified()
if restart {
err = s.Restart()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
return
}
}
}
type upstreamJSON struct {
Upstreams []string `json:"upstream_dns"` // Upstreams
BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS
AllServers bool `json:"all_servers"` // --all-servers param for dnsproxy
}
func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request) {
req := upstreamJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Failed to parse new upstreams config json: %s", err)
return
}
if len(req.Upstreams) != 0 {
err = ValidateUpstreams(req.Upstreams)
if err != nil {
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
return
}
}
newconf := FilteringConfig{}
newconf.UpstreamDNS = req.Upstreams
// bootstrap servers are plain DNS only
for _, host := range req.BootstrapDNS {
if err := checkPlainDNS(host); err != nil {
httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
return
}
}
newconf.BootstrapDNS = req.BootstrapDNS
newconf.AllServers = req.AllServers
s.Lock()
s.conf.UpstreamDNS = newconf.UpstreamDNS
s.conf.BootstrapDNS = newconf.BootstrapDNS
s.conf.AllServers = newconf.AllServers
s.Unlock()
s.conf.ConfigModified()
err = s.Restart()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
return
}
}
// ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
func ValidateUpstreams(upstreams []string) error {
var defaultUpstreamFound bool
for _, u := range upstreams {
d, err := validateUpstream(u)
if err != nil {
return err
}
// Check this flag until default upstream will not be found
if !defaultUpstreamFound {
defaultUpstreamFound = d
}
}
// Return error if there are no default upstreams
if !defaultUpstreamFound {
return fmt.Errorf("no default upstreams specified")
}
return nil
}
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
func validateUpstream(u string) (bool, error) {
// Check if user tries to specify upstream for domain
u, defaultUpstream, err := separateUpstream(u)
if err != nil {
return defaultUpstream, err
}
// The special server address '#' means "use the default servers"
if u == "#" && !defaultUpstream {
return defaultUpstream, nil
}
// Check if the upstream has a valid protocol prefix
for _, proto := range protocols {
if strings.HasPrefix(u, proto) {
return defaultUpstream, nil
}
}
// Return error if the upstream contains '://' without any valid protocol
if strings.Contains(u, "://") {
return defaultUpstream, fmt.Errorf("wrong protocol")
}
// Check if upstream is valid plain DNS
return defaultUpstream, checkPlainDNS(u)
}
// separateUpstream returns upstream without specified domains and a bool flag that indicates if no domains were specified
// error will be returned if upstream per domain specification is invalid
func separateUpstream(upstream string) (string, bool, error) {
defaultUpstream := true
if strings.HasPrefix(upstream, "[/") {
defaultUpstream = false
// split domains and upstream string
domainsAndUpstream := strings.Split(strings.TrimPrefix(upstream, "[/"), "/]")
if len(domainsAndUpstream) != 2 {
return "", defaultUpstream, fmt.Errorf("wrong DNS upstream per domain specification: %s", upstream)
}
// split domains list and validate each one
for _, host := range strings.Split(domainsAndUpstream[0], "/") {
if host != "" {
if err := utils.IsValidHostname(host); err != nil {
return "", defaultUpstream, err
}
}
}
upstream = domainsAndUpstream[1]
}
return upstream, defaultUpstream, nil
}
// checkPlainDNS checks if host is plain DNS
func checkPlainDNS(upstream string) error {
// Check if host is ip without port
if net.ParseIP(upstream) != nil {
return nil
}
// Check if host is ip with port
ip, port, err := net.SplitHostPort(upstream)
if err != nil {
return err
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("%s is not a valid IP", ip)
}
_, err = strconv.ParseInt(port, 0, 64)
if err != nil {
return fmt.Errorf("%s is not a valid port: %s", port, err)
}
return nil
}
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
req := upstreamJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
return
}
if len(req.Upstreams) == 0 {
httpError(r, w, http.StatusBadRequest, "No servers specified")
return
}
result := map[string]string{}
for _, host := range req.Upstreams {
err = checkDNS(host, req.BootstrapDNS)
if err != nil {
log.Info("%v", err)
result[host] = err.Error()
} else {
result[host] = "OK"
}
}
jsonVal, err := json.Marshal(result)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
return
}
}
func checkDNS(input string, bootstrap []string) error {
// separate upstream from domains list
input, defaultUpstream, err := separateUpstream(input)
if err != nil {
return fmt.Errorf("wrong upstream format: %s", err)
}
// No need to check this entrance
if input == "#" && !defaultUpstream {
return nil
}
if _, err := validateUpstream(input); err != nil {
return fmt.Errorf("wrong upstream format: %s", err)
}
if len(bootstrap) == 0 {
bootstrap = defaultBootstrap
}
log.Debug("Checking if DNS %s works...", input)
u, err := upstream.AddressToUpstream(input, upstream.Options{Bootstrap: bootstrap, Timeout: DefaultTimeout})
if err != nil {
return fmt.Errorf("failed to choose upstream for %s: %s", input, err)
}
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := u.Exchange(&req)
if err != nil {
return fmt.Errorf("couldn't communicate with DNS server %s: %s", input, err)
}
if len(reply.Answer) != 1 {
return fmt.Errorf("DNS server %s returned wrong answer", input)
}
if t, ok := reply.Answer[0].(*dns.A); ok {
if !net.IPv4(8, 8, 8, 8).Equal(t.A) {
return fmt.Errorf("DNS server %s returned wrong answer: %v", input, t.A)
}
}
log.Debug("DNS %s works OK", input)
return nil
}
func (s *Server) registerHandlers() {
s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig)
s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig)
s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)
s.conf.HTTPRegister("POST", "/control/access/set", s.handleAccessSet)
}

View File

@@ -16,7 +16,9 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
const (
@@ -26,7 +28,7 @@ const (
func TestServer(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -60,7 +62,7 @@ func TestServer(t *testing.T) {
func TestServerWithProtectionDisabled(t *testing.T) {
s := createTestServer(t)
s.conf.ProtectionEnabled = false
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -92,8 +94,9 @@ func TestDotServer(t *testing.T) {
PrivateKeyData: keyPem,
}
_ = s.Prepare(nil)
// Starting the server
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -125,7 +128,7 @@ func TestDotServer(t *testing.T) {
func TestServerRace(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -148,7 +151,7 @@ func TestServerRace(t *testing.T) {
func TestSafeSearch(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -189,7 +192,7 @@ func TestSafeSearch(t *testing.T) {
func TestInvalidRequest(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -215,7 +218,7 @@ func TestInvalidRequest(t *testing.T) {
func TestBlockedRequest(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -245,10 +248,146 @@ func TestBlockedRequest(t *testing.T) {
}
}
// testUpstream is a mock of real upstream.
// specify fields with necessary values to simulate real upstream behaviour
type testUpstream struct {
cn map[string]string // Map of [name]canonical_name
ipv4 map[string][]net.IP // Map of [name]IPv4
ipv6 map[string][]net.IP // Map of [name]IPv6
}
func (u *testUpstream) Exchange(m *dns.Msg) (*dns.Msg, error) {
resp := dns.Msg{}
resp.SetReply(m)
hasARecord := false
hasAAAARecord := false
reqType := m.Question[0].Qtype
name := m.Question[0].Name
// Let's check if we have any CNAME for given name
if cname, ok := u.cn[name]; ok {
cn := dns.CNAME{}
cn.Hdr.Name = name
cn.Hdr.Rrtype = dns.TypeCNAME
cn.Target = cname
resp.Answer = append(resp.Answer, &cn)
}
// Let's check if we can add some A records to the answer
if ipv4addr, ok := u.ipv4[name]; ok && reqType == dns.TypeA {
hasARecord = true
for _, ipv4 := range ipv4addr {
respA := dns.A{}
respA.Hdr.Rrtype = dns.TypeA
respA.Hdr.Name = name
respA.A = ipv4
resp.Answer = append(resp.Answer, &respA)
}
}
// Let's check if we can add some AAAA records to the answer
if u.ipv6 != nil {
if ipv6addr, ok := u.ipv6[name]; ok && reqType == dns.TypeAAAA {
hasAAAARecord = true
for _, ipv6 := range ipv6addr {
respAAAA := dns.A{}
respAAAA.Hdr.Rrtype = dns.TypeAAAA
respAAAA.Hdr.Name = name
respAAAA.A = ipv6
resp.Answer = append(resp.Answer, &respAAAA)
}
}
}
if len(resp.Answer) == 0 {
if hasARecord || hasAAAARecord {
// Set No Error RCode if there are some records for given Qname but we didn't apply them
resp.SetRcode(m, dns.RcodeSuccess)
} else {
// Set NXDomain RCode otherwise
resp.SetRcode(m, dns.RcodeNameError)
}
}
return &resp, nil
}
func (u *testUpstream) Address() string {
return "test"
}
func (s *Server) startWithUpstream(u upstream.Upstream) error {
s.Lock()
defer s.Unlock()
err := s.Prepare(nil)
if err != nil {
return err
}
s.dnsProxy.Upstreams = []upstream.Upstream{u}
return s.dnsProxy.Start()
}
// testCNAMEs is a simple map of names and CNAMEs necessary for the testUpstream work
var testCNAMEs = map[string]string{
"badhost.": "null.example.org.",
"whitelist.example.org.": "null.example.org.",
}
// testIPv4 is a simple map of names and IPv4s necessary for the testUpstream work
var testIPv4 = map[string][]net.IP{
"null.example.org.": {{1, 2, 3, 4}},
"example.org.": {{127, 0, 0, 255}},
}
func TestBlockCNAME(t *testing.T) {
s := createTestServer(t)
testUpstm := &testUpstream{testCNAMEs, testIPv4, nil}
err := s.startWithUpstream(testUpstm)
assert.True(t, err == nil)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
// 'badhost' has a canonical name 'null.example.org' which is blocked by filters:
// response is blocked
req := dns.Msg{}
req.Id = dns.Id()
req.Question = []dns.Question{
{Name: "badhost.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := dns.Exchange(&req, addr.String())
assert.True(t, err == nil)
assert.True(t, reply.Rcode == dns.RcodeNameError)
// 'whitelist.example.org' has a canonical name 'null.example.org' which is blocked by filters
// but 'whitelist.example.org' is in a whitelist:
// response isn't blocked
req = dns.Msg{}
req.Id = dns.Id()
req.Question = []dns.Question{
{Name: "whitelist.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err = dns.Exchange(&req, addr.String())
assert.True(t, err == nil)
assert.True(t, reply.Rcode == dns.RcodeSuccess)
// 'example.org' has a canonical name 'cname1' with IP 127.0.0.255 which is blocked by filters:
// response is blocked
req = dns.Msg{}
req.Id = dns.Id()
req.Question = []dns.Question{
{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err = dns.Exchange(&req, addr.String())
assert.True(t, err == nil)
assert.True(t, reply.Rcode == dns.RcodeNameError)
_ = s.Stop()
}
func TestNullBlockedRequest(t *testing.T) {
s := createTestServer(t)
s.conf.FilteringConfig.BlockingMode = "null_ip"
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -285,9 +424,58 @@ func TestNullBlockedRequest(t *testing.T) {
}
}
func TestBlockedCustomIP(t *testing.T) {
rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1 host.example.org\n@@||whitelist.example.org^\n||127.0.0.255\n"
filters := map[int]string{}
filters[0] = rules
c := dnsfilter.Config{}
f := dnsfilter.New(&c, filters)
s := NewServer(f, nil, nil)
conf := ServerConfig{}
conf.UDPListenAddr = &net.UDPAddr{Port: 0}
conf.TCPListenAddr = &net.TCPAddr{Port: 0}
conf.ProtectionEnabled = true
conf.BlockingMode = "custom_ip"
conf.BlockingIPv4 = "bad IP"
conf.UpstreamDNS = []string{"8.8.8.8:53", "8.8.4.4:53"}
err := s.Prepare(&conf)
assert.True(t, err != nil) // invalid BlockingIPv4
conf.BlockingIPv4 = "0.0.0.1"
conf.BlockingIPv6 = "::1"
err = s.Prepare(&conf)
assert.True(t, err == nil)
err = s.Start()
assert.True(t, err == nil, "%s", err)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
req := createTestMessageWithType("null.example.org.", dns.TypeA)
reply, err := dns.Exchange(req, addr.String())
assert.True(t, err == nil)
assert.True(t, len(reply.Answer) == 1)
a, ok := reply.Answer[0].(*dns.A)
assert.True(t, ok)
assert.True(t, a.A.String() == "0.0.0.1")
req = createTestMessageWithType("null.example.org.", dns.TypeAAAA)
reply, err = dns.Exchange(req, addr.String())
assert.True(t, err == nil)
assert.True(t, len(reply.Answer) == 1)
a6, ok := reply.Answer[0].(*dns.AAAA)
assert.True(t, ok)
assert.True(t, a6.AAAA.String() == "::1")
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestBlockedByHosts(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -326,7 +514,7 @@ func TestBlockedByHosts(t *testing.T) {
func TestBlockedBySafeBrowsing(t *testing.T) {
s := createTestServer(t)
err := s.Start(nil)
err := s.Start()
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
@@ -375,7 +563,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
}
func createTestServer(t *testing.T) *Server {
rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1 host.example.org\n"
rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1 host.example.org\n@@||whitelist.example.org^\n||127.0.0.255\n"
filters := map[int]string{}
filters[0] = rules
c := dnsfilter.Config{}
@@ -390,9 +578,10 @@ func createTestServer(t *testing.T) *Server {
s := NewServer(f, nil, nil)
s.conf.UDPListenAddr = &net.UDPAddr{Port: 0}
s.conf.TCPListenAddr = &net.TCPAddr{Port: 0}
s.conf.FilteringConfig.FilteringEnabled = true
s.conf.UpstreamDNS = []string{"8.8.8.8:53", "8.8.4.4:53"}
s.conf.FilteringConfig.ProtectionEnabled = true
err := s.Prepare(nil)
assert.True(t, err == nil)
return s
}
@@ -512,6 +701,16 @@ func createTestMessage(host string) *dns.Msg {
return &req
}
func createTestMessageWithType(host string, qtype uint16) *dns.Msg {
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: host, Qtype: qtype, Qclass: dns.ClassINET},
}
return &req
}
func assertGoogleAResponse(t *testing.T, reply *dns.Msg) {
assertResponse(t, reply, "8.8.8.8")
}
@@ -541,67 +740,118 @@ func publicKey(priv interface{}) interface{} {
}
func TestIsBlockedIPAllowed(t *testing.T) {
s := createTestServer(t)
s.conf.AllowedClients = []string{"1.1.1.1", "2.2.0.0/16"}
a := &accessCtx{}
assert.True(t, a.Init([]string{"1.1.1.1", "2.2.0.0/16"}, nil, nil) == nil)
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
if s.isBlockedIP("1.1.1.1") {
t.Fatalf("isBlockedIP")
}
if !s.isBlockedIP("1.1.1.2") {
t.Fatalf("isBlockedIP")
}
if s.isBlockedIP("2.2.1.1") {
t.Fatalf("isBlockedIP")
}
if !s.isBlockedIP("2.3.1.1") {
t.Fatalf("isBlockedIP")
}
assert.True(t, !a.IsBlockedIP("1.1.1.1"))
assert.True(t, a.IsBlockedIP("1.1.1.2"))
assert.True(t, !a.IsBlockedIP("2.2.1.1"))
assert.True(t, a.IsBlockedIP("2.3.1.1"))
}
func TestIsBlockedIPDisallowed(t *testing.T) {
s := createTestServer(t)
s.conf.DisallowedClients = []string{"1.1.1.1", "2.2.0.0/16"}
a := &accessCtx{}
assert.True(t, a.Init(nil, []string{"1.1.1.1", "2.2.0.0/16"}, nil) == nil)
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
if !s.isBlockedIP("1.1.1.1") {
t.Fatalf("isBlockedIP")
}
if s.isBlockedIP("1.1.1.2") {
t.Fatalf("isBlockedIP")
}
if !s.isBlockedIP("2.2.1.1") {
t.Fatalf("isBlockedIP")
}
if s.isBlockedIP("2.3.1.1") {
t.Fatalf("isBlockedIP")
}
assert.True(t, a.IsBlockedIP("1.1.1.1"))
assert.True(t, !a.IsBlockedIP("1.1.1.2"))
assert.True(t, a.IsBlockedIP("2.2.1.1"))
assert.True(t, !a.IsBlockedIP("2.3.1.1"))
}
func TestIsBlockedIPBlockedDomain(t *testing.T) {
s := createTestServer(t)
s.conf.BlockedHosts = []string{"host1", "host2"}
a := &accessCtx{}
assert.True(t, a.Init(nil, nil, []string{"host1", "host2"}) == nil)
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
assert.True(t, a.IsBlockedDomain("host1"))
assert.True(t, a.IsBlockedDomain("host2"))
assert.True(t, !a.IsBlockedDomain("host3"))
}
func TestValidateUpstream(t *testing.T) {
invalidUpstreams := []string{"1.2.3.4.5",
"123.3.7m",
"htttps://google.com/dns-query",
"[/host.com]tls://dns.adguard.com",
"[host.ru]#",
}
if !s.isBlockedDomain("host1") {
t.Fatalf("isBlockedDomain")
validDefaultUpstreams := []string{"1.1.1.1",
"tls://1.1.1.1",
"https://dns.adguard.com/dns-query",
"sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
if !s.isBlockedDomain("host2") {
t.Fatalf("isBlockedDomain")
validUpstreams := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
if s.isBlockedDomain("host3") {
t.Fatalf("isBlockedDomain")
for _, u := range invalidUpstreams {
_, err := validateUpstream(u)
if err == nil {
t.Fatalf("upstream %s is invalid but it pass through validation", u)
}
}
for _, u := range validDefaultUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if !defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
for _, u := range validUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
}
func TestValidateUpstreamsSet(t *testing.T) {
// Set of valid upstreams. There is no default upstream specified
upstreamsSet := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
err := ValidateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is no default upstream")
}
// Let's add default upstream
upstreamsSet = append(upstreamsSet, "8.8.8.8")
err = ValidateUpstreams(upstreamsSet)
if err != nil {
t.Fatalf("upstreams set is valid, but doesn't pass through validation cause: %s", err)
}
// Let's add invalid upstream
upstreamsSet = append(upstreamsSet, "dhcp://fake.dns")
err = ValidateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is an invalid upstream in set, but it pass through validation")
}
}
func TestIpFromAddr(t *testing.T) {
addr := net.UDPAddr{}
addr.IP = net.ParseIP("1:2:3::4")
addr.Port = 12345
addr.Zone = "eth0"
a := ipFromAddr(&addr)
assert.True(t, a == "1:2:3::4")
a = ipFromAddr(nil)
assert.True(t, a == "")
}

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

BIN
doc/agh-filtering.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

13
go.mod
View File

@@ -1,15 +1,14 @@
module github.com/AdguardTeam/AdGuardHome
go 1.12
go 1.13
require (
github.com/AdguardTeam/dnsproxy v0.19.6
github.com/AdguardTeam/golibs v0.2.4
github.com/AdguardTeam/urlfilter v0.6.1
github.com/AdguardTeam/dnsproxy v0.23.3
github.com/AdguardTeam/golibs v0.3.0
github.com/AdguardTeam/urlfilter v0.7.0
github.com/NYTimes/gziphandler v1.1.1
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
github.com/etcd-io/bbolt v1.3.3
github.com/go-test/deep v1.0.4
github.com/go-test/deep v1.0.4 // indirect
github.com/gobuffalo/packr v1.19.0
github.com/joomcode/errorx v1.0.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
@@ -18,7 +17,7 @@ require (
github.com/miekg/dns v1.1.19
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
github.com/stretchr/testify v1.4.0
go.etcd.io/bbolt v1.3.3
go.etcd.io/bbolt v1.3.3 // indirect
golang.org/x/crypto v0.0.0-20191001170739-f9e2070545dc
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0
golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed

69
go.sum
View File

@@ -1,11 +1,12 @@
github.com/AdguardTeam/dnsproxy v0.19.6 h1:66hBrCMDIlaMXu8AiB4fHXxnX7Q6MDIuUn7ghxGR8gY=
github.com/AdguardTeam/dnsproxy v0.19.6/go.mod h1:61N+71pTD0+nRQWBc0BNh3Ayl3NIW+KZ1sF2+ZObsSA=
github.com/AdguardTeam/golibs v0.1.3 h1:hmapdTtMtIk3T8eQDwTOLdqZLGDKNKk9325uC8z12xg=
github.com/AdguardTeam/golibs v0.1.3/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
github.com/AdguardTeam/dnsproxy v0.23.3 h1:RzI9M0sX99t7qnlikvKTPW25sCFzgfBStxUJ+2z1KQI=
github.com/AdguardTeam/dnsproxy v0.23.3/go.mod h1:2qy8rpdfBzKgMPxkHmPdaNK4XZJ322v4KtVGI8s8Bn0=
github.com/AdguardTeam/golibs v0.2.4 h1:GUssokegKxKF13K67Pgl0ZGwqHjNN6X7sep5ik6ORdY=
github.com/AdguardTeam/golibs v0.2.4/go.mod h1:R3M+mAg3nWG4X4Hsag5eef/TckHFH12ZYhK7AzJc8+U=
github.com/AdguardTeam/urlfilter v0.6.1 h1:JX3gNYmgD9TCWE+G0C4MOn8WHYLAoVt0agltSvfldkY=
github.com/AdguardTeam/urlfilter v0.6.1/go.mod h1:y+XdxBdbRG9v7pfjznlvv4Ufi2HTG8D0YMqR22OVy0Y=
github.com/AdguardTeam/golibs v0.3.0 h1:1zO8ulGEOdXDDM++Ap4sYfTsT/Z4tZBZtiWSA4ykcOU=
github.com/AdguardTeam/golibs v0.3.0/go.mod h1:R3M+mAg3nWG4X4Hsag5eef/TckHFH12ZYhK7AzJc8+U=
github.com/AdguardTeam/gomitmproxy v0.1.2/go.mod h1:Mrt/3EfiXIYY2aZ7KsLuCUJzUARD/fWJ119IfzOB13M=
github.com/AdguardTeam/urlfilter v0.7.0 h1:ffFLt4rA3GX8PJYGL3bGcT5bSxZlML5k6cKpSeN2UI8=
github.com/AdguardTeam/urlfilter v0.7.0/go.mod h1:GHXPzEG59ezyff22lXSQ7dicj1kFZBrH5kmZ6EvQzfk=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 h1:UUppSQnhf4Yc6xGxSkoQpPhb7RVzuv5Nb1mwJ5VId9s=
@@ -16,26 +17,34 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/ameshkov/dnscrypt v1.0.7 h1:7LS9wiC/6c00H3ZdZOlwQSYGTJvs12g5ui9D1VSZ2aQ=
github.com/ameshkov/dnscrypt v1.0.7/go.mod h1:rA74ASZ0j4JqPWaiN64hN97QXJ/zu5Kb2xgn295VzWQ=
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/ameshkov/goproxy v0.0.0-20190328085534-e9f6fabc24d4/go.mod h1:tKA6C/1BQYejT7L6ZX0klDrqloYenYETv3BCk8xCbrQ=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
@@ -46,9 +55,10 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -58,10 +68,14 @@ github.com/joomcode/errorx v0.8.0 h1:GhAqPtcYuo1O7TOIbtzEIDzPGQ3SrKJ3tdjXNmUtDNo
github.com/joomcode/errorx v0.8.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
github.com/joomcode/errorx v1.0.0 h1:RJAKLTy1Sv2Tszhu14m5RZP4VGRlhXutG/XlL1En5VM=
github.com/joomcode/errorx v1.0.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b h1:vfiqKno48aUndBMjTeWFpCExNnTf2Xnd6d228L4EfTQ=
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b/go.mod h1:10UU/bEkzh2iEN6aYzbevY7J6p03KO5siTxQWXMEerg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -71,23 +85,38 @@ github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32B
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.19 h1:0ymbfaLG1/utH2+BydNiF+dx1jSEmdr/nylOtkGHZZg=
github.com/miekg/dns v1.1.19/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM=
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.19.9+incompatible h1:IrPVlK4nfwW10DF7pW+7YJKws9NkgNzWozwwWv9FsgY=
github.com/shirou/gopsutil v2.19.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
@@ -95,6 +124,7 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
@@ -103,7 +133,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/arch v0.0.0-20190815191158-8a70ba74b3a1/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
@@ -112,11 +142,11 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191001170739-f9e2070545dc h1:KyTYo8xkh/2WdbFLUyQwBS0Jfn3qfZ9QmuPbok2oENE=
golang.org/x/crypto v0.0.0-20191001170739-f9e2070545dc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -126,15 +156,19 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190122071731-054c452bb702 h1:Lk4tbZFnlyPgV+sLgTw5yGfzrlOn9kx4vSombi2FFlY=
golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed h1:5TJcLJn2a55mJjzYk0yOoqN8X1OdvBDUnaZaKKyQtkY=
@@ -146,11 +180,12 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -13,12 +13,12 @@ import (
"time"
"github.com/AdguardTeam/golibs/log"
"go.etcd.io/bbolt"
"github.com/etcd-io/bbolt"
"golang.org/x/crypto/bcrypt"
)
const cookieTTL = 365 * 24 // in hours
const expireTime = 30 * 24 // in hours
const sessionCookieName = "agh_session"
type session struct {
userName string
@@ -56,10 +56,11 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object
type Auth struct {
db *bbolt.DB
sessions map[string]*session // session name -> session data
lock sync.Mutex
users []User
db *bbolt.DB
sessions map[string]*session // session name -> session data
lock sync.Mutex
users []User
sessionTTL uint32 // in seconds
}
// User object
@@ -69,8 +70,9 @@ type User struct {
}
// InitAuth - create a global object
func InitAuth(dbFilename string, users []User) *Auth {
func InitAuth(dbFilename string, users []User, sessionTTL uint32) *Auth {
a := Auth{}
a.sessionTTL = sessionTTL
a.sessions = make(map[string]*session)
rand.Seed(time.Now().UTC().Unix())
var err error
@@ -145,18 +147,21 @@ func (a *Auth) loadSessions() {
// store session data in file
func (a *Auth) addSession(data []byte, s *session) {
name := hex.EncodeToString(data)
a.lock.Lock()
a.sessions[hex.EncodeToString(data)] = s
a.sessions[name] = s
a.lock.Unlock()
a.storeSession(data, s)
if a.storeSession(data, s) {
log.Info("Auth: created session %s: expire=%d", name, s.expire)
}
}
// store session data in file
func (a *Auth) storeSession(data []byte, s *session) {
func (a *Auth) storeSession(data []byte, s *session) bool {
tx, err := a.db.Begin(true)
if err != nil {
log.Error("Auth: bbolt.Begin: %s", err)
return
return false
}
defer func() {
_ = tx.Rollback()
@@ -165,21 +170,20 @@ func (a *Auth) storeSession(data []byte, s *session) {
bkt, err := tx.CreateBucketIfNotExists(bucketName())
if err != nil {
log.Error("Auth: bbolt.CreateBucketIfNotExists: %s", err)
return
return false
}
err = bkt.Put(data, s.serialize())
if err != nil {
log.Error("Auth: bbolt.Put: %s", err)
return
return false
}
err = tx.Commit()
if err != nil {
log.Error("Auth: bbolt.Commit: %s", err)
return
return false
}
log.Debug("Auth: stored session in DB")
return true
}
// remove session from file
@@ -233,7 +237,7 @@ func (a *Auth) CheckSession(sess string) int {
return 1
}
newExpire := now + expireTime*60*60
newExpire := now + a.sessionTTL
if s.expire/(24*60*60) != newExpire/(24*60*60) {
// update expiration time once a day
update = true
@@ -244,7 +248,9 @@ func (a *Auth) CheckSession(sess string) int {
if update {
key, _ := hex.DecodeString(sess)
a.storeSession(key, s)
if a.storeSession(key, s) {
log.Debug("Auth: updated session %s: expire=%d", sess, s.expire)
}
}
return 0
@@ -270,8 +276,8 @@ func getSession(u *User) []byte {
return hash[:]
}
func httpCookie(req loginJSON) string {
u := config.auth.UserFind(req.Name, req.Password)
func (a *Auth) httpCookie(req loginJSON) string {
u := a.UserFind(req.Name, req.Password)
if len(u.Name) == 0 {
return ""
}
@@ -286,10 +292,11 @@ func httpCookie(req loginJSON) string {
s := session{}
s.userName = u.Name
s.expire = uint32(now.Unix()) + expireTime*60*60
config.auth.addSession(sess, &s)
s.expire = uint32(now.Unix()) + a.sessionTTL
a.addSession(sess, &s)
return fmt.Sprintf("session=%s; Path=/; HttpOnly; Expires=%s", hex.EncodeToString(sess), expstr)
return fmt.Sprintf("%s=%s; Path=/; HttpOnly; Expires=%s",
sessionCookieName, hex.EncodeToString(sess), expstr)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
@@ -300,10 +307,11 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
cookie := httpCookie(req)
cookie := config.auth.httpCookie(req)
if len(cookie) == 0 {
log.Info("Auth: invalid user name or password: name='%s'", req.Name)
time.Sleep(1 * time.Second)
httpError(w, http.StatusBadRequest, "invalid login or password")
http.Error(w, "invalid user name or password", http.StatusBadRequest)
return
}
@@ -324,7 +332,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/login.html")
s := fmt.Sprintf("session=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT")
s := fmt.Sprintf("%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
sessionCookieName)
w.Header().Set("Set-Cookie", s)
w.WriteHeader(http.StatusFound)
@@ -344,7 +353,7 @@ func parseCookie(cookie string) string {
if len(kv) != 2 {
continue
}
if kv[0] == "session" {
if kv[0] == sessionCookieName {
return kv[1]
}
}
@@ -357,7 +366,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
if r.URL.Path == "/login.html" {
// redirect to dashboard if already authenticated
authRequired := config.auth != nil && config.auth.AuthRequired()
cookie, err := r.Cookie("session")
cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil {
r := config.auth.CheckSession(cookie.Value)
if r == 0 {
@@ -365,7 +374,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
w.WriteHeader(http.StatusFound)
return
} else if r < 0 {
log.Debug("Auth: invalid cookie value: %s", cookie)
log.Info("Auth: invalid cookie value: %s", cookie)
}
}
@@ -376,13 +385,13 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
} else if config.auth != nil && config.auth.AuthRequired() {
// redirect to login page if not authenticated
ok := false
cookie, err := r.Cookie("session")
cookie, err := r.Cookie(sessionCookieName)
if err == nil {
r := config.auth.CheckSession(cookie.Value)
if r == 0 {
ok = true
} else if r < 0 {
log.Debug("Auth: invalid cookie value: %s", cookie)
log.Info("Auth: invalid cookie value: %s", cookie)
}
} else {
// there's no Cookie, check Basic authentication
@@ -391,6 +400,8 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
u := config.auth.UserFind(user, pass)
if len(u.Name) != 0 {
ok = true
} else {
log.Info("Auth: invalid Basic Authorization value")
}
}
}
@@ -452,7 +463,7 @@ func (a *Auth) UserFind(login string, password string) User {
// GetCurrentUser - get the current user
func (a *Auth) GetCurrentUser(r *http.Request) User {
cookie, err := r.Cookie("session")
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// there's no Cookie, check Basic authentication
user, pass, ok := r.BasicAuth()

View File

@@ -27,7 +27,7 @@ func TestAuth(t *testing.T) {
users := []User{
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
}
a := InitAuth(fn, nil)
a := InitAuth(fn, nil, 60)
s := session{}
user := User{Name: "name"}
@@ -54,7 +54,7 @@ func TestAuth(t *testing.T) {
a.Close()
// load saved session
a = InitAuth(fn, users)
a = InitAuth(fn, users, 60)
// the session is still alive
assert.True(t, a.CheckSession(sessStr) == 0)
@@ -69,7 +69,7 @@ func TestAuth(t *testing.T) {
time.Sleep(3 * time.Second)
// load and remove expired sessions
a = InitAuth(fn, users)
a = InitAuth(fn, users, 60)
assert.True(t, a.CheckSession(sessStr) == -1)
a.Close()
@@ -100,7 +100,7 @@ func TestAuthHTTP(t *testing.T) {
users := []User{
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
}
config.auth = InitAuth(fn, users)
config.auth = InitAuth(fn, users, 60)
handlerCalled := false
handler := func(w http.ResponseWriter, r *http.Request) {
@@ -129,7 +129,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
// perform login
cookie := httpCookie(loginJSON{Name: "name", Password: "password"})
cookie := config.auth.httpCookie(loginJSON{Name: "name", Password: "password"})
assert.True(t, cookie != "")
// get /

View File

@@ -6,10 +6,10 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
)
var serviceRules map[string][]*urlfilter.NetworkRule // service name -> filtering rules
var serviceRules map[string][]*rules.NetworkRule // service name -> filtering rules
type svc struct {
name string
@@ -20,24 +20,88 @@ type svc struct {
// client/src/helpers/constants.js
// client/src/components/ui/Icons.js
var serviceRulesArray = []svc{
{"whatsapp", []string{"||whatsapp.net^"}},
{"facebook", []string{"||facebook.com^"}},
{"whatsapp", []string{"||whatsapp.net^", "||whatsapp.com^"}},
{"facebook", []string{"||facebook.com^", "||facebook.net^", "||fbcdn.net^", "||fb.me^", "||fb.com^", "||fbsbx.com^"}},
{"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}},
{"youtube", []string{"||youtube.com^", "||ytimg.com^"}},
{"messenger", []string{"||fb.com^", "||facebook.com^"}},
{"youtube", []string{"||youtube.com^", "||ytimg.com^", "||youtu.be^", "||googlevideo.com^", "||youtubei.googleapis.com^"}},
{"messenger", []string{"||fb.com^", "||facebook.com^", "||messenger.com^"}},
{"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}},
{"netflix", []string{"||nflxext.com^", "||netflix.com^"}},
{"instagram", []string{"||instagram.com^"}},
{"snapchat", []string{"||snapchat.com^"}},
{"instagram", []string{"||instagram.com^", "||cdninstagram.com^"}},
{"snapchat", []string{"||snapchat.com^", "||sc-cdn.net^", "||impala-media-production.s3.amazonaws.com^"}},
{"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^"}},
{"ok", []string{"||ok.ru^"}},
{"skype", []string{"||skype.com^"}},
{"vk", []string{"||vk.com^"}},
{"origin", []string{"||origin.com^", "||signin.ea.com^", "||accounts.ea.com^"}},
{"steam", []string{"||steam.com^"}},
{"epic_games", []string{"||epicgames.com^"}},
{"reddit", []string{"||reddit.com^", "||redditstatic.com^", "||redditmedia.com^", "||redd.it^"}},
{"mail_ru", []string{"||mail.ru^"}},
{"cloudflare", []string{
"||cloudflare.com^",
"||cloudflare-dns.com^",
"||cloudflare.net^",
"||cloudflareinsights.com^",
"||cloudflarestream.com^",
"||cloudflareresolve.com^",
"||cloudflareclient.com^",
"||cloudflarebolt.com^",
"||cloudflarestatus.com^",
"||cloudflare.cn^",
"||one.one^",
"||warp.plus^",
}},
{"amazon", []string{
"||amazon.com^",
"||media-amazon.com^",
"||images-amazon.com^",
"||a2z.com^",
"||amazon.ae^",
"||amazon.ca^",
"||amazon.cn^",
"||amazon.de^",
"||amazon.es^",
"||amazon.fr^",
"||amazon.in^",
"||amazon.it^",
"||amazon.nl^",
"||amazon.com.au^",
"||amazon.com.br^",
"||amazon.co.jp^",
"||amazon.com.mx^",
"||amazon.co.uk^",
}},
{"ebay", []string{
"||ebay.com^",
"||ebayimg.com^",
"||ebaystatic.com^",
"||ebaycdn.net^",
"||ebayinc.com^",
"||ebay.at^",
"||ebay.be^",
"||ebay.ca^",
"||ebay.ch^",
"||ebay.cn^",
"||ebay.de^",
"||ebay.es^",
"||ebay.fr^",
"||ebay.ie^",
"||ebay.in^",
"||ebay.it^",
"||ebay.ph^",
"||ebay.pl^",
"||ebay.nl^",
"||ebay.com.au^",
"||ebay.com.cn^",
"||ebay.com.hk^",
"||ebay.com.my^",
"||ebay.com.sg^",
"||ebay.co.uk^",
}},
{"tiktok", []string{
"||tiktok.com^",
"||tiktokcdn.com^",
"||snssdk.com^",
"||amemv.com^",
"||toutiao.com^",
@@ -50,23 +114,25 @@ var serviceRulesArray = []svc{
"||bytecdn.cn^",
"||byteimg.com^",
"||ixigua.com^",
"||muscdn.com^",
"||bytedance.map.fastly.net^",
}},
}
// convert array to map
func initServices() {
serviceRules = make(map[string][]*urlfilter.NetworkRule)
serviceRules = make(map[string][]*rules.NetworkRule)
for _, s := range serviceRulesArray {
rules := []*urlfilter.NetworkRule{}
netRules := []*rules.NetworkRule{}
for _, text := range s.rules {
rule, err := urlfilter.NewNetworkRule(text, 0)
rule, err := rules.NewNetworkRule(text, 0)
if err != nil {
log.Error("urlfilter.NewNetworkRule: %s rule: %s", err, text)
log.Error("rules.NewNetworkRule: %s rule: %s", err, text)
continue
}
rules = append(rules, rule)
netRules = append(netRules, rule)
}
serviceRules[s.name] = rules
serviceRules[s.name] = netRules
}
}
@@ -121,7 +187,7 @@ func handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
return
}
returnOK(w)
httpOK(r, w)
}
// RegisterBlockedServicesHandlers - register HTTP handlers

View File

@@ -1,11 +1,10 @@
package home
import (
"encoding/json"
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"runtime"
@@ -13,6 +12,8 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils"
)
@@ -23,8 +24,7 @@ const (
// Client information
type Client struct {
IP string
MAC string
IDs []string
Name string
UseOwnSettings bool // false: use global settings
FilteringEnabled bool
@@ -35,22 +35,8 @@ type Client struct {
UseOwnBlockedServices bool // false: use global settings
BlockedServices []string
}
type clientJSON struct {
IP string `json:"ip"`
MAC string `json:"mac"`
Name string `json:"name"`
UseGlobalSettings bool `json:"use_global_settings"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
WhoisInfo map[string]interface{} `json:"whois_info"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
BlockedServices []string `json:"blocked_services"`
Upstreams []string // list of upstream servers to be used for the client's requests
}
type clientSource uint
@@ -74,22 +60,98 @@ type ClientHost struct {
type clientsContainer struct {
list map[string]*Client // name -> client
ipIndex map[string]*Client // IP -> client
idIndex map[string]*Client // IP -> client
ipHost map[string]*ClientHost // IP -> Hostname
lock sync.Mutex
dhcpServer *dhcpd.Server
testing bool // if TRUE, this object is used for internal tests
}
// Init initializes clients container
// Note: this function must be called only once
func (clients *clientsContainer) Init() {
func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.Server) {
if clients.list != nil {
log.Fatal("clients.list != nil")
}
clients.list = make(map[string]*Client)
clients.ipIndex = make(map[string]*Client)
clients.idIndex = make(map[string]*Client)
clients.ipHost = make(map[string]*ClientHost)
clients.dhcpServer = dhcpServer
clients.addFromConfig(objects)
go clients.periodicUpdate()
if !clients.testing {
go clients.periodicUpdate()
clients.registerWebHandlers()
}
}
type clientObject struct {
Name string `yaml:"name"`
IDs []string `yaml:"ids"`
UseGlobalSettings bool `yaml:"use_global_settings"`
FilteringEnabled bool `yaml:"filtering_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
BlockedServices []string `yaml:"blocked_services"`
Upstreams []string `yaml:"upstreams"`
}
func (clients *clientsContainer) addFromConfig(objects []clientObject) {
for _, cy := range objects {
cli := Client{
Name: cy.Name,
IDs: cy.IDs,
UseOwnSettings: !cy.UseGlobalSettings,
FilteringEnabled: cy.FilteringEnabled,
ParentalEnabled: cy.ParentalEnabled,
SafeSearchEnabled: cy.SafeSearchEnabled,
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
BlockedServices: cy.BlockedServices,
Upstreams: cy.Upstreams,
}
_, err := clients.Add(cli)
if err != nil {
log.Tracef("clientAdd: %s", err)
}
}
}
// WriteDiskConfig - write configuration
func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
clients.lock.Lock()
for _, cli := range clients.list {
cy := clientObject{
Name: cli.Name,
UseGlobalSettings: !cli.UseOwnSettings,
FilteringEnabled: cli.FilteringEnabled,
ParentalEnabled: cli.ParentalEnabled,
SafeSearchEnabled: cli.SafeSearchEnabled,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
}
cy.IDs = make([]string, len(cli.IDs))
copy(cy.IDs, cli.IDs)
cy.BlockedServices = make([]string, len(cli.BlockedServices))
copy(cy.BlockedServices, cli.BlockedServices)
cy.Upstreams = make([]string, len(cli.Upstreams))
copy(cy.Upstreams, cli.Upstreams)
*objects = append(*objects, cy)
}
clients.lock.Unlock()
}
func (clients *clientsContainer) periodicUpdate() {
@@ -101,17 +163,12 @@ func (clients *clientsContainer) periodicUpdate() {
}
}
// GetList returns the pointer to clients list
func (clients *clientsContainer) GetList() map[string]*Client {
return clients.list
}
// Exists checks if client with this IP already exists
func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.ipIndex[ip]
_, ok := clients.idIndex[ip]
if ok {
return true
}
@@ -128,25 +185,45 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
// Find searches for a client by IP
func (clients *clientsContainer) Find(ip string) (Client, bool) {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return Client{}, false
}
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.ipIndex[ip]
c, ok := clients.idIndex[ip]
if ok {
return *c, true
}
for _, c = range clients.list {
if len(c.MAC) != 0 {
mac, err := net.ParseMAC(c.MAC)
for _, id := range c.IDs {
_, ipnet, err := net.ParseCIDR(id)
if err != nil {
continue
}
ipAddr := config.dhcpServer.FindIPbyMAC(mac)
if ipAddr == nil {
if ipnet.Contains(ipAddr) {
return *c, true
}
}
}
if clients.dhcpServer == nil {
return Client{}, false
}
macFound := clients.dhcpServer.FindMACbyIP(ipAddr)
if macFound == nil {
return Client{}, false
}
for _, c = range clients.list {
for _, id := range c.IDs {
hwAddr, err := net.ParseMAC(id)
if err != nil {
continue
}
if ip == ipAddr.String() {
if bytes.Equal(hwAddr, macFound) {
return *c, true
}
}
@@ -155,29 +232,60 @@ func (clients *clientsContainer) Find(ip string) (Client, bool) {
return Client{}, false
}
// FindAutoClient - search for an auto-client by IP
func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return ClientHost{}, false
}
clients.lock.Lock()
defer clients.lock.Unlock()
ch, ok := clients.ipHost[ip]
if ok {
return *ch, true
}
return ClientHost{}, false
}
// Check if Client object's fields are correct
func (c *Client) check() error {
if len(c.Name) == 0 {
return fmt.Errorf("Invalid Name")
}
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
(len(c.IP) != 0 && len(c.MAC) != 0) {
return fmt.Errorf("IP or MAC required")
if len(c.IDs) == 0 {
return fmt.Errorf("ID required")
}
if len(c.IP) != 0 {
ip := net.ParseIP(c.IP)
if ip == nil {
return fmt.Errorf("Invalid IP")
for i, id := range c.IDs {
ip := net.ParseIP(id)
if ip != nil {
c.IDs[i] = ip.String() // normalize IP address
continue
}
c.IP = ip.String()
} else {
_, err := net.ParseMAC(c.MAC)
_, _, err := net.ParseCIDR(id)
if err == nil {
continue
}
_, err = net.ParseMAC(id)
if err == nil {
continue
}
return fmt.Errorf("Invalid ID: %s", id)
}
if len(c.Upstreams) != 0 {
err := dnsforward.ValidateUpstreams(c.Upstreams)
if err != nil {
return fmt.Errorf("Invalid MAC: %s", err)
return fmt.Errorf("Invalid upstream servers: %s", err)
}
}
return nil
}
@@ -198,26 +306,34 @@ func (clients *clientsContainer) Add(c Client) (bool, error) {
return false, nil
}
// check IP index
if len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
// check ID index
for _, id := range c.IDs {
c2, ok := clients.idIndex[id]
if ok {
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
return false, fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
}
}
ch, ok := clients.ipHost[c.IP]
if ok {
c.WhoisInfo = ch.WhoisInfo
delete(clients.ipHost, c.IP)
// remove auto-clients with the same IP address, keeping WHOIS info if possible
for _, id := range c.IDs {
ch, ok := clients.ipHost[id]
if ok {
if len(c.WhoisInfo) == 0 {
c.WhoisInfo = ch.WhoisInfo
}
delete(clients.ipHost, id)
}
}
// update Name index
clients.list[c.Name] = &c
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
// update ID index
for _, id := range c.IDs {
clients.idIndex[id] = &c
}
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
log.Tracef("'%s': ID:%v [%d]", c.Name, c.IDs, len(clients.list))
return true, nil
}
@@ -231,8 +347,26 @@ func (clients *clientsContainer) Del(name string) bool {
return false
}
// update Name index
delete(clients.list, name)
delete(clients.ipIndex, c.IP)
// update ID index
for _, id := range c.IDs {
delete(clients.idIndex, id)
}
return true
}
// Return TRUE if arrays are equal
func arraysEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i != len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
@@ -260,27 +394,30 @@ func (clients *clientsContainer) Update(name string, c Client) error {
}
// check IP index
if old.IP != c.IP && len(c.IP) != 0 {
c2, ok := clients.ipIndex[c.IP]
if ok {
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
if !arraysEqual(old.IDs, c.IDs) {
for _, id := range c.IDs {
c2, ok := clients.idIndex[id]
if ok && c2 != old {
return fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
}
}
// update ID index
for _, id := range old.IDs {
delete(clients.idIndex, id)
}
for _, id := range c.IDs {
clients.idIndex[id] = old
}
}
// update Name index
if old.Name != c.Name {
delete(clients.list, old.Name)
}
clients.list[c.Name] = &c
// update IP index
if old.IP != c.IP {
delete(clients.ipIndex, old.IP)
}
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
clients.list[c.Name] = old
}
*old = c
return nil
}
@@ -289,7 +426,7 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.ipIndex[ip]
c, ok := clients.idIndex[ip]
if ok {
c.WhoisInfo = info
log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo)
@@ -319,7 +456,7 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (
defer clients.lock.Unlock()
// check index
_, ok := clients.ipIndex[ip]
_, ok := clients.idIndex[ip]
if ok {
return false, nil
}
@@ -427,223 +564,19 @@ func (clients *clientsContainer) addFromSystemARP() {
// add clients from DHCP that have non-empty Hostname property
func (clients *clientsContainer) addFromDHCP() {
leases := config.dhcpServer.Leases()
if clients.dhcpServer == nil {
return
}
leases := clients.dhcpServer.Leases()
n := 0
for _, l := range leases {
if len(l.Hostname) == 0 {
continue
}
ok, _ := config.clients.AddHost(l.IP.String(), l.Hostname, ClientSourceDHCP)
ok, _ := clients.AddHost(l.IP.String(), l.Hostname, ClientSourceDHCP)
if ok {
n++
}
}
log.Debug("Added %d client aliases from DHCP", n)
}
type clientHostJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
Source string `json:"source"`
WhoisInfo map[string]interface{} `json:"whois_info"`
}
type clientListJSON struct {
Clients []clientJSON `json:"clients"`
AutoClients []clientHostJSON `json:"auto_clients"`
}
// respond with information about configured clients
func handleGetClients(w http.ResponseWriter, r *http.Request) {
data := clientListJSON{}
config.clients.lock.Lock()
for _, c := range config.clients.list {
cj := clientJSON{
IP: c.IP,
MAC: c.MAC,
Name: c.Name,
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled,
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
BlockedServices: c.BlockedServices,
}
if len(c.MAC) != 0 {
hwAddr, _ := net.ParseMAC(c.MAC)
ipAddr := config.dhcpServer.FindIPbyMAC(hwAddr)
if ipAddr != nil {
cj.IP = ipAddr.String()
}
}
cj.WhoisInfo = make(map[string]interface{})
for _, wi := range c.WhoisInfo {
cj.WhoisInfo[wi[0]] = wi[1]
}
data.Clients = append(data.Clients, cj)
}
for ip, ch := range config.clients.ipHost {
cj := clientHostJSON{
IP: ip,
Name: ch.Host,
}
cj.Source = "etc/hosts"
switch ch.Source {
case ClientSourceDHCP:
cj.Source = "DHCP"
case ClientSourceRDNS:
cj.Source = "rDNS"
case ClientSourceARP:
cj.Source = "ARP"
case ClientSourceWHOIS:
cj.Source = "WHOIS"
}
cj.WhoisInfo = make(map[string]interface{})
for _, wi := range ch.WhoisInfo {
cj.WhoisInfo[wi[0]] = wi[1]
}
data.AutoClients = append(data.AutoClients, cj)
}
config.clients.lock.Unlock()
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
return
}
}
// Convert JSON object to Client object
func jsonToClient(cj clientJSON) (*Client, error) {
c := Client{
IP: cj.IP,
MAC: cj.MAC,
Name: cj.Name,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices,
}
return &c, nil
}
// Add a new client
func handleAddClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
c, err := jsonToClient(cj)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
ok, err := config.clients.Add(*c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
if !ok {
httpError(w, http.StatusBadRequest, "Client already exists")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// Remove client
func handleDelClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil || len(cj.Name) == 0 {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if !config.clients.Del(cj.Name) {
httpError(w, http.StatusBadRequest, "Client not found")
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
type updateJSON struct {
Name string `json:"name"`
Data clientJSON `json:"data"`
}
// Update client's properties
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
var dj updateJSON
err = json.Unmarshal(body, &dj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if len(dj.Name) == 0 {
httpError(w, http.StatusBadRequest, "Invalid request")
return
}
c, err := jsonToClient(dj.Data)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = config.clients.Update(dj.Name, *c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
_ = writeAllConfigsAndReloadDNS()
returnOK(w)
}
// RegisterClientsHandlers registers HTTP handlers
func RegisterClientsHandlers() {
httpRegister(http.MethodGet, "/control/clients", handleGetClients)
httpRegister(http.MethodPost, "/control/clients/add", handleAddClient)
httpRegister(http.MethodPost, "/control/clients/delete", handleDelClient)
httpRegister(http.MethodPost, "/control/clients/update", handleUpdateClient)
}

289
home/clients_http.go Normal file
View File

@@ -0,0 +1,289 @@
package home
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type clientJSON struct {
IDs []string `json:"ids"`
Name string `json:"name"`
UseGlobalSettings bool `json:"use_global_settings"`
FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"`
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
WhoisInfo map[string]interface{} `json:"whois_info"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
BlockedServices []string `json:"blocked_services"`
Upstreams []string `json:"upstreams"`
}
type clientHostJSON struct {
IP string `json:"ip"`
Name string `json:"name"`
Source string `json:"source"`
WhoisInfo map[string]interface{} `json:"whois_info"`
}
type clientListJSON struct {
Clients []clientJSON `json:"clients"`
AutoClients []clientHostJSON `json:"auto_clients"`
}
// respond with information about configured clients
func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) {
data := clientListJSON{}
clients.lock.Lock()
for _, c := range clients.list {
cj := clientToJSON(c)
data.Clients = append(data.Clients, cj)
}
for ip, ch := range clients.ipHost {
cj := clientHostJSON{
IP: ip,
Name: ch.Host,
}
cj.Source = "etc/hosts"
switch ch.Source {
case ClientSourceDHCP:
cj.Source = "DHCP"
case ClientSourceRDNS:
cj.Source = "rDNS"
case ClientSourceARP:
cj.Source = "ARP"
case ClientSourceWHOIS:
cj.Source = "WHOIS"
}
cj.WhoisInfo = make(map[string]interface{})
for _, wi := range ch.WhoisInfo {
cj.WhoisInfo[wi[0]] = wi[1]
}
data.AutoClients = append(data.AutoClients, cj)
}
clients.lock.Unlock()
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
return
}
}
// Convert JSON object to Client object
func jsonToClient(cj clientJSON) (*Client, error) {
c := Client{
Name: cj.Name,
IDs: cj.IDs,
UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
}
return &c, nil
}
// Convert Client object to JSON
func clientToJSON(c *Client) clientJSON {
cj := clientJSON{
Name: c.Name,
IDs: c.IDs,
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled,
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
BlockedServices: c.BlockedServices,
Upstreams: c.Upstreams,
}
cj.WhoisInfo = make(map[string]interface{})
for _, wi := range c.WhoisInfo {
cj.WhoisInfo[wi[0]] = wi[1]
}
return cj
}
type clientHostJSONWithID struct {
IDs []string `json:"ids"`
Name string `json:"name"`
WhoisInfo map[string]interface{} `json:"whois_info"`
}
// Convert ClientHost object to JSON
func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID {
cj := clientHostJSONWithID{
Name: ch.Host,
IDs: []string{ip},
}
cj.WhoisInfo = make(map[string]interface{})
for _, wi := range ch.WhoisInfo {
cj.WhoisInfo[wi[0]] = wi[1]
}
return cj
}
// Add a new client
func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
c, err := jsonToClient(cj)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
ok, err := clients.Add(*c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
if !ok {
httpError(w, http.StatusBadRequest, "Client already exists")
return
}
onConfigModified()
}
// Remove client
func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
cj := clientJSON{}
err = json.Unmarshal(body, &cj)
if err != nil || len(cj.Name) == 0 {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if !clients.Del(cj.Name) {
httpError(w, http.StatusBadRequest, "Client not found")
return
}
onConfigModified()
}
type updateJSON struct {
Name string `json:"name"`
Data clientJSON `json:"data"`
}
// Update client's properties
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
return
}
var dj updateJSON
err = json.Unmarshal(body, &dj)
if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return
}
if len(dj.Name) == 0 {
httpError(w, http.StatusBadRequest, "Invalid request")
return
}
c, err := jsonToClient(dj.Data)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = clients.Update(dj.Name, *c)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
onConfigModified()
}
// Get the list of clients by IP address list
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data := []map[string]interface{}{}
for i := 0; ; i++ {
ip := q.Get(fmt.Sprintf("ip%d", i))
if len(ip) == 0 {
break
}
el := map[string]interface{}{}
c, ok := clients.Find(ip)
if !ok {
ch, ok := clients.FindAutoClient(ip)
if !ok {
continue // a client with this IP isn't found
}
cj := clientHostToJSON(ip, ch)
el[ip] = cj
} else {
cj := clientToJSON(&c)
el[ip] = cj
}
data = append(data, el)
}
js, err := json.Marshal(data)
if err != nil {
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(js)
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write response: %s", err)
}
}
// RegisterClientsHandlers registers HTTP handlers
func (clients *clientsContainer) registerWebHandlers() {
httpRegister("GET", "/control/clients", clients.handleGetClients)
httpRegister("POST", "/control/clients/add", clients.handleAddClient)
httpRegister("POST", "/control/clients/delete", clients.handleDelClient)
httpRegister("POST", "/control/clients/update", clients.handleUpdateClient)
httpRegister("GET", "/control/clients/find", clients.handleFindClient)
}

View File

@@ -11,12 +11,13 @@ func TestClients(t *testing.T) {
var e error
var b bool
clients := clientsContainer{}
clients.testing = true
clients.Init()
clients.Init(nil, nil)
// add
c = Client{
IP: "1.1.1.1",
IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa"},
Name: "client1",
}
b, e = clients.Add(c)
@@ -26,7 +27,7 @@ func TestClients(t *testing.T) {
// add #2
c = Client{
IP: "2.2.2.2",
IDs: []string{"2.2.2.2"},
Name: "client2",
}
b, e = clients.Add(c)
@@ -35,18 +36,17 @@ func TestClients(t *testing.T) {
}
c, b = clients.Find("1.1.1.1")
if !b || c.Name != "client1" {
t.Fatalf("Find #1")
}
assert.True(t, b && c.Name == "client1")
c, b = clients.Find("1:2:3::4")
assert.True(t, b && c.Name == "client1")
c, b = clients.Find("2.2.2.2")
if !b || c.Name != "client2" {
t.Fatalf("Find #2")
}
assert.True(t, b && c.Name == "client2")
// failed add - name in use
c = Client{
IP: "1.2.3.5",
IDs: []string{"1.2.3.5"},
Name: "client1",
}
b, _ = clients.Add(c)
@@ -56,7 +56,7 @@ func TestClients(t *testing.T) {
// failed add - ip in use
c = Client{
IP: "2.2.2.2",
IDs: []string{"2.2.2.2"},
Name: "client3",
}
b, e = clients.Add(c)
@@ -70,35 +70,45 @@ func TestClients(t *testing.T) {
assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile))
// failed update - no such name
c.IP = "1.2.3.0"
c.IDs = []string{"1.2.3.0"}
c.Name = "client3"
if clients.Update("client3", c) == nil {
t.Fatalf("Update")
}
// failed update - name in use
c.IP = "1.2.3.0"
c.IDs = []string{"1.2.3.0"}
c.Name = "client2"
if clients.Update("client1", c) == nil {
t.Fatalf("Update - name in use")
}
// failed update - ip in use
c.IP = "2.2.2.2"
c.IDs = []string{"2.2.2.2"}
c.Name = "client1"
if clients.Update("client1", c) == nil {
t.Fatalf("Update - ip in use")
}
// update
c.IP = "1.1.1.2"
c.IDs = []string{"1.1.1.2"}
c.Name = "client1"
if clients.Update("client1", c) != nil {
t.Fatalf("Update")
}
// get after update
assert.True(t, !(clients.Exists("1.1.1.1", ClientSourceHostsFile) || !clients.Exists("1.1.1.2", ClientSourceHostsFile)))
assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile))
assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile))
// update - rename
c.IDs = []string{"1.1.1.2"}
c.Name = "client1-renamed"
c.UseOwnSettings = true
assert.True(t, clients.Update("client1", c) == nil)
c = Client{}
c, b = clients.Find("1.1.1.2")
assert.True(t, b && c.Name == "client1-renamed" && c.IDs[0] == "1.1.1.2" && c.UseOwnSettings)
// failed remove - no such name
if clients.Del("client3") {
@@ -106,7 +116,7 @@ func TestClients(t *testing.T) {
}
// remove
assert.True(t, !(!clients.Del("client1") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
assert.True(t, !(!clients.Del("client1-renamed") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
// add host client
b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP)
@@ -139,7 +149,8 @@ func TestClients(t *testing.T) {
func TestClientsWhois(t *testing.T) {
var c Client
clients := clientsContainer{}
clients.Init()
clients.testing = true
clients.Init(nil, nil)
whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}}
// set whois info on new client
@@ -153,11 +164,11 @@ func TestClientsWhois(t *testing.T) {
// set whois info on existing client
c = Client{
IP: "1.1.1.2",
IDs: []string{"1.1.1.2"},
Name: "client1",
}
_, _ = clients.Add(c)
clients.SetWhoisInfo("1.1.1.2", whois)
assert.True(t, clients.ipIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
assert.True(t, clients.idIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
_ = clients.Del("client1")
}

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