Compare commits

...

20 Commits

Author SHA1 Message Date
Ainar Garipov
1a693f790b Pull request: client: upd i18n
Merge in DNS/adguard-home from 2643-upd-i18n to master

Squashed commit of the following:

commit e965f59e87ca4ac3da69bac6a1d0526d091ea02a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jul 7 13:30:43 2021 +0300

    client: upd i18n
2021-07-07 13:44:53 +03:00
Ainar Garipov
1571609e59 Pull request #1238: dnsforward: fix panic
Updates #3318.

Squashed commit of the following:

commit eebaa3ac3a6bfcb29e280006ead8ea9554b8a3bf
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 6 14:59:00 2021 +0300

    dnsforward: imp code

commit 7591a3b183ab95f27f908267321518740cb7d4bb
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 6 14:55:33 2021 +0300

    dnsforward: fix panic
2021-07-06 15:08:55 +03:00
Ainar Garipov
4b5a66ee61 Pull request #1237: all: upd dnsproxy
Updates #3315.

Squashed commit of the following:

commit f91b979b4b9eb8141e7320a54992786a699aa437
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 6 13:48:29 2021 +0300

    all: upd dnsproxy
2021-07-06 14:12:57 +03:00
Eugene Burkov
e113b276e7 Pull request: 2504 querylog interval
Merge in DNS/adguard-home from 2504-querylog-ivl to master

Updates #2504.

Squashed commit of the following:

commit 5d15a6f735cd195fc81c8af909b56fbc7db1fe21
Merge: 8cd5c30d 97073d0d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 18:45:10 2021 +0300

    Merge branch 'master' into 2504-querylog-ivl

commit 8cd5c30de6f72d4b12162dbc9e3d90132795fe94
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 18:35:50 2021 +0300

    client: fix fmt

commit e95d462c31d886bacec0735acc567fec7c962149
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 17:58:06 2021 +0300

    home: imp code

commit 48737b249c52a997a4f34dac45fbaf699477b007
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 17:23:18 2021 +0300

    home: imp duration

commit 44f5dc3d3ada5120d74caa24cace9a253b8f15d3
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 16:55:31 2021 +0300

    home: imp code, docs

commit bb2826521b7e5d248ce2ab686528219c312b8ba2
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 16:11:40 2021 +0300

    all: imp code, docs

commit d688aed1f340807a8bac8807c263956b0fc16f5b
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jul 1 13:49:42 2021 +0300

    all: change querylog interval setting format
2021-07-01 18:50:28 +03:00
Ainar Garipov
97073d0d9e Pull request: all: fix typos in chlog
Merge in DNS/adguard-home from imp-changelog to master

Squashed commit of the following:

commit 4a1ed06b77d4aa39c59e1b83fb019709a4362088
Merge: e1352bea 116bedd7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 30 14:04:50 2021 +0300

    Merge branch 'master' into imp-changelog

commit e1352bea546211a5dd84890c51f32bdf69174850
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 30 13:59:46 2021 +0300

    all: fix typos in chlog
2021-06-30 14:10:06 +03:00
Eugene Burkov
116bedd727 Pull request: 3012 idna search
Merge in DNS/adguard-home from 3012-idna-search to master

Closes #3012.

Squashed commit of the following:

commit 6a9fbfe16860df5db5982a70cfbf040967b6e6ae
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 21:28:10 2021 +0300

    querylog: add todo

commit 31292ba1aeb9e91ff4f6abae7ffdf806a87cae66
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 21:21:46 2021 +0300

    querylog: imp docs, code

commit 35757f76837cb8034f6079a351d01aa4706bfea7
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 21:01:08 2021 +0300

    queerylog: fix idn case match

commit eecfc98b6449c5c7c5a23602e80e47002034bc25
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 20:32:00 2021 +0300

    querylog: imp code, docs

commit 8aa6242fe92a9c2daa674b976595b13be96b0cf7
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 20:00:54 2021 +0300

    querylog: sup idn search
2021-06-30 11:04:48 +03:00
Ainar Garipov
232cd381ff Pull request: client: add links to the query log from dashboard
Updates #3245.

Squashed commit of the following:

commit 32ca6e12f34e78ab51501f27609ed21fd5a1d1e7
Merge: b5cd0bee 77c70193
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 17:06:50 2021 +0300

    Merge branch 'master' into 3245-imp-dashboard

commit b5cd0beea03d39a65f97922e3cdbc6af3e19ffdc
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 29 16:16:45 2021 +0300

    client: remove links from labels

commit 3a3003aeed737819888a6974c878b88aa6abee38
Merge: 2dc1a5f8 90a85362
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 22 13:42:21 2021 +0300

    Merge branch 'master' into 3245-imp-dashboard

commit 2dc1a5f8b936c771cb0c7c26a529c824d0e445a2
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Jun 11 19:24:12 2021 +0300

    client: get response status from const

commit 1bb9dc2abd6e9f2263fca09fed4d393f1969446c
Merge: 137dcb05 12f1e4ed
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Jun 11 19:08:24 2021 +0300

    Merge branch 'master' into 3245-imp-dashboard

commit 137dcb058e
Author: Mark Hicken <mhicken@adobe.com>
Date:   Tue Jun 8 15:05:17 2021 -0600

    Add links to all the dashboard things
2021-06-29 17:13:34 +03:00
Ainar Garipov
77c701930e Pull request: client: upd i18n
Merge in DNS/adguard-home from upd-i18n to master

Squashed commit of the following:

commit 74b5028ba58b5e9e76c5d2c36e04e64699733095
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 16:16:29 2021 +0300

    client: upd i18n
2021-06-29 17:04:36 +03:00
Ainar Garipov
e08a64ebe4 Pull request: all: allow clientid in access settings
Updates #2624.
Updates #3162.

Squashed commit of the following:

commit 68860da717a23a0bfeba14b7fe10b5e4ad38726d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 15:41:33 2021 +0300

    all: imp types, names

commit ebd4ec26636853d0d58c4e331e6a78feede20813
Merge: 239eb721 16e5e09c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 15:14:33 2021 +0300

    Merge branch 'master' into 2624-clientid-access

commit 239eb7215abc47e99a0300a0f4cf56002689b1a9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 15:13:10 2021 +0300

    all: fix client blocking check

commit e6bece3ea8367b3cbe3d90702a3368c870ad4f13
Merge: 9935f2a3 9d1656b5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 29 13:12:28 2021 +0300

    Merge branch 'master' into 2624-clientid-access

commit 9935f2a30bcfae2b853f3ef610c0ab7a56a8f448
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 29 11:26:51 2021 +0300

    client: show block button for client id

commit ed786a6a74a081cd89e9d67df3537a4fadd54831
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 25 15:56:23 2021 +0300

    client: imp i18n

commit 4fed21c68473ad408960c08a7d87624cabce1911
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 25 15:34:09 2021 +0300

    all: imp i18n, docs

commit 55e65c0d6b939560c53dcb834a4557eb3853d194
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 25 13:34:01 2021 +0300

    all: fix cache, imp code, docs, tests

commit c1e5a83e76deb44b1f92729bb9ddfcc6a96ac4a8
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 24 19:27:12 2021 +0300

    all: allow clientid in access settings
2021-06-29 15:53:28 +03:00
Eugene Burkov
16e5e09c2e Pull request: 3013 querylog idna
Merge in DNS/adguard-home from 3013-idna to master

Closes #3013.

Squashed commit of the following:

commit 567d4c3beef3cf3ee995ad9d8c3aba6616c74c6c
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 13:11:10 2021 +0300

    client: mv punycode label

commit 6585dcaece9f590d7f02afb5aa25953ab0c2555b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 29 12:32:40 2021 +0300

    client: handle unicode name

commit c0f61bfbb9bdf919be7b07c112c4b7a52f3ad6a1
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 20:00:57 2021 +0300

    all: imp log of changes

commit 41388abc8770ce164bcba327fcf0013133b5e6f7
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 19:52:23 2021 +0300

    scripts: imp hooks

commit 9c4ba933fbd9340e1de061d4f451218238650c0f
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 19:47:27 2021 +0300

    all: imp code, docs

commit 61bd6d6f926480cb8c2f9bd3cd2b61e1532f71cf
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 16:09:25 2021 +0300

    querylog: add ascii hostname, convert to unicode
2021-06-29 13:36:52 +03:00
Ainar Garipov
9d1656b5c1 Pull request: all: imp hacking, hooks
Merge in DNS/adguard-home from imp-hacking to master

Squashed commit of the following:

commit fcc0740526948256b5b356a3a7969e00baa35ff7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 28 18:16:57 2021 +0300

    all: imp hacking, hooks
2021-06-28 18:24:33 +03:00
Eugene Burkov
28f34ca399 Pull request: 3257 source directive
Merge in DNS/adguard-home from 3257-ifaces-source to master

Updates #3257.

Squashed commit of the following:

commit 0b9b42bab731bbd048e97893cf209794ea014dfe
Merge: 530a1a23 e25a5329
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 16:53:36 2021 +0300

    Merge branch 'master' into 3257-ifaces-source

commit 530a1a23a601c5575c8dc5f6f97cd84801cf911b
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Jun 25 19:43:55 2021 +0300

    aghnet: imp code, add docs

commit 58de84821b93bcbb3df1834ba33fbad817201b1d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Jun 25 13:34:43 2021 +0300

    aghnet: sup "source" directive

commit c0901abd5212902295e8ee546fad652092fdb5a8
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Jun 24 16:46:03 2021 +0300

    aghos: mv func to aghnet
2021-06-28 17:02:45 +03:00
Ainar Garipov
e25a532987 Pull request: all: rm inactive list, imp lint script
Updates #2644.

Squashed commit of the following:

commit 1a1c2f0a232885d8ea6335bce2831a5a21fe800a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 25 18:17:51 2021 +0300

    all: rm inactive list, imp lint script
2021-06-25 18:30:30 +03:00
Ainar Garipov
90a853627e Pull request: all: upd dnsproxy
Updates #3217.

Squashed commit of the following:

commit c0208c858bbf92177b0624d30049b33dd32499ce
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 22 13:23:17 2021 +0300

    all: upd dnsproxy
2021-06-22 13:33:35 +03:00
Ainar Garipov
dab7b439d1 Pull request: all: imp docs, names
Merge in DNS/adguard-home from imp-text to master

Squashed commit of the following:

commit fa7d64014fb2ac379e1c137eaccc7aefca86419d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 11 17:09:00 2021 +0300

    all: imp docs, names
2021-06-18 18:13:36 +03:00
Ainar Garipov
3ee0369cb9 Pull request: all: imp ipset, add tests
Closes #2611.

Squashed commit of the following:

commit f72577757e5cd0299863ccc01780ad9307adc6ea
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 18 17:32:41 2021 +0300

    dnsforward: imp err msgs

commit ed8d6dd4b5d7171dfbd57742b53bf96be6cbec29
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 18 17:00:49 2021 +0300

    all: imp ipset code

commit 887b3268bae496f4ad616b277f5e28d3ee24a370
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 18 16:09:08 2021 +0300

    dnsforward: imp ipset, add tests
2021-06-18 17:55:01 +03:00
Ainar Garipov
dbe8b92dfc Pull request: all: make stats disableable, imp code
Updates #2141.

Squashed commit of the following:

commit d8f3bcd9927b00a1d4b8b60b43144bc4b4469311
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 17 19:10:53 2021 +0300

    stats: imp docs, names

commit 97eae3c2da5585467ca024bdacdbf922bcc8b444
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 17 18:51:49 2021 +0300

    all: make stats disableable, imp code
2021-06-17 19:44:46 +03:00
Ainar Garipov
5104b79cf6 Pull request: client: add reset leases btn
Updates #1691.

Squashed commit of the following:

commit 2c48fb956aba28eae47071c9f7f4d579dde12955
Merge: 38f5191b 7547d3a4
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 17 14:28:23 2021 +0300

    Merge branch 'master' into 1691-dhcp-reset-form

commit 38f5191bcd62eb53e4663fccdfc2a60247881931
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jun 17 13:14:59 2021 +0300

    client: handle dhcp leases reset

commit a97df17028ca640fd32b4d9762aa54fb381df7e5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 16 17:12:10 2021 +0300

    client: add reset leases btn
2021-06-17 14:32:33 +03:00
Ainar Garipov
7547d3a422 Pull request: all: sup many ips in host rules
Closes #1381.

Squashed commit of the following:

commit 44965f74d8becb27173cafc533e56e1cde484b59
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 17 13:54:44 2021 +0300

    dnsforward: imp code, docs

commit 23736cf9b407b668faade19b61739536caafff03
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 16 20:33:59 2021 +0300

    dnsforward: rm todo

commit ff086756160d72f3cdbe662862dd1fb447ac5ff3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 16 20:32:01 2021 +0300

    all: sup many ips in host rules
2021-06-17 14:09:16 +03:00
Ainar Garipov
84e71e912e Pull request: dhcpd: add purge, imp code
Updates #1691.

Squashed commit of the following:

commit 2ce6cc005d09ac7d63e4e575705d86e92046697f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 16 16:42:57 2021 +0300

    dhcp: imp code

commit 8f2bd7048d864e10aaed9e7338c0bbe852699a31
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 16 16:27:06 2021 +0300

    dhcpd: add purge, imp code
2021-06-16 16:48:46 +03:00
130 changed files with 3894 additions and 2007 deletions

View File

@@ -20,7 +20,7 @@ Please answer the following questions for yourself before submitting an issue. *
* **Version of AdGuard Home server:**
* <!-- (e.g. v0.123.4) -->
* **How did you install AdGuard Home:**
* <!-- (e.g. Built from source, Snapcraft, Docker, Github releases, etc.) -->
* <!-- (e.g. Built from source, Snapcraft, Docker, GitHub releases, etc.) -->
* **How did you setup DNS configuration:**
* <!-- (System/Router/IoT) -->
* **If it's a router or IoT, please write device model:**

View File

@@ -15,8 +15,16 @@ and this project adheres to
### Added
- The ability to set the timeout for querying the upstream servers ([#2280]).
- The ability to change group and user ID on startup on Unix ([#2763]).
- New possible value of `6h` for `querylog_interval` setting ([#2504]).
- Blocking access using client IDs ([#2624], [#3162]).
- `source` directives support in `/etc/network/interfaces` on Linux ([#3257]).
- RFC 9000 support in DNS-over-QUIC.
- Completely disabling statistics by setting the statistics interval to zero
([#2141]).
- The ability to completely purge DHCP leases ([#1691]).
- Settable timeouts for querying the upstream servers ([#2280]).
- Configuration file parameters to change group and user ID on startup on Unix
([#2763]).
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439]).
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]).
@@ -28,11 +36,17 @@ and this project adheres to
([#3185]).
- The ability to completely disable reverse DNS resolving of IPs from
locally-served networks ([#3184]).
- New flag `--local-frontend` to serve dinamically changeable frontend files
- New flag `--local-frontend` to serve dynamically changeable frontend files
from disk as opposed to the ones that were compiled into the binary.
### Changed
- `querylog_interval` setting is now formatted in hours.
- Query log search now supports internationalized domains ([#3012]).
- Internationalized domains are now shown decoded in the query log with the
original encoded version shown in request details ([#3013]).
- When /etc/hosts-type rules have several IPs for one host, all IPs are now
returned instead of only the first one ([#1381]).
- The setting `rlimit_nofile` is now in the `os` block of the configuration
file, together with the new `group` and `user` settings ([#2763]).
- Permissions on filter files are now `0o644` instead of `0o600` ([#3198]).
@@ -48,6 +62,7 @@ released by then.
### Fixed
- Occasional breakages on network errors with DNS-over-HTTP upstreams ([#3217]).
- Errors when setting static IP on Linux ([#3257]).
- Treatment of domain names and FQDNs in custom rules with `$dnsrewrite` that
use the `PTR` type ([#3256]).
@@ -62,12 +77,20 @@ released by then.
- Go 1.15 support.
[#1381]: https://github.com/AdguardTeam/AdGuardHome/issues/1381
[#1691]: https://github.com/AdguardTeam/AdGuardHome/issues/1691
[#2141]: https://github.com/AdguardTeam/AdGuardHome/issues/2141
[#2280]: https://github.com/AdguardTeam/AdGuardHome/issues/2280
[#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439
[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443
[#2504]: https://github.com/AdguardTeam/AdGuardHome/issues/2504
[#2624]: https://github.com/AdguardTeam/AdGuardHome/issues/2624
[#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763
[#3012]: https://github.com/AdguardTeam/AdGuardHome/issues/3012
[#3013]: https://github.com/AdguardTeam/AdGuardHome/issues/3013
[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136
[#3162]: https://github.com/AdguardTeam/AdGuardHome/issues/3162
[#3166]: https://github.com/AdguardTeam/AdGuardHome/issues/3166
[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172
[#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184
@@ -75,6 +98,7 @@ released by then.
[#3186]: https://github.com/AdguardTeam/AdGuardHome/issues/3186
[#3194]: https://github.com/AdguardTeam/AdGuardHome/issues/3194
[#3198]: https://github.com/AdguardTeam/AdGuardHome/issues/3198
[#3217]: https://github.com/AdguardTeam/AdGuardHome/issues/3217
[#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256
[#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257
@@ -128,7 +152,7 @@ released by then.
### Fixed
- Local domain name handling when the DHCP server is disabled ([#3028]).
- Normalization of perviously-saved invalid static DHCP leases ([#3027]).
- Normalization of previously-saved invalid static DHCP leases ([#3027]).
- Validation of IPv6 addresses with zones in system resolvers ([#3022]).
[#3022]: https://github.com/AdguardTeam/AdGuardHome/issues/3022

View File

@@ -77,7 +77,8 @@ attributes to make it work in Markdown renderers that strip "id". -->
* Avoid `init` and use explicit initialization functions instead.
* Avoid `new`, especially with structs.
* Avoid `new`, especially with structs, unless a temporary value is needed,
for example when checking the type of an error using `errors.As`.
* Check against empty strings like this:
@@ -98,8 +99,9 @@ attributes to make it work in Markdown renderers that strip "id". -->
* Constructors should validate their arguments and return meaningful errors.
As a corollary, avoid lazy initialization.
* Define `MarshalFoo` methods on non-pointer receivers, as pointer receivers
[can have surprising results][staticcheck-911].
* Prefer to define methods on pointer receievers, unless the type is small or
a non-pointer receiever is required, for example `MarshalFoo` methods (see
[staticcheck-911]).
* Don't mix horizontal and vertical placement of arguments in function and
method calls. That is, either this:
@@ -118,6 +120,15 @@ attributes to make it work in Markdown renderers that strip "id". -->
)
```
Or, with a struct literal:
```go
err := functionWithALongName(arg, structType{
field1: val1,
field2: val2,
})
```
But **never** this:
```go
@@ -131,8 +142,8 @@ attributes to make it work in Markdown renderers that strip "id". -->
as well.
* Don't use `fmt.Sprintf` where a more structured approach to string
conversion could be used. For example, `net.JoinHostPort` or
`url.(*URL).String`.
conversion could be used. For example, `aghnet.JoinHostPort`,
`net.JoinHostPort` or `url.(*URL).String`.
* Don't use naked `return`s.
@@ -148,12 +159,17 @@ attributes to make it work in Markdown renderers that strip "id". -->
* Minimize scope of variables as much as possible.
* No shadowing, since it can often lead to subtle bugs, especially with
errors.
* No name shadowing, including of predeclared identifiers, since it can often
lead to subtle bugs, especially with errors. This rule does not apply to
struct fields, since they are always used together with the name of the
struct value, so there isn't any confusion.
* Prefer constants to variables where possible. Avoid global variables. Use
[constant errors] instead of `errors.New`.
* Prefer defining `Foo.String` and `ParseFoo` in terms of `Foo.MarshalText`
and `Foo.UnmarshalText` correspondingly and not the other way around.
* Prefer to use named functions for goroutines.
* Program code lines should not be longer than one hundred (**100**) columns.
@@ -197,8 +213,8 @@ attributes to make it work in Markdown renderers that strip "id". -->
### <a href="#formatting" id="formatting" name="formatting">Formatting</a>
* Decorate `break`, `continue`, `fallthrough`, `return`, and other terminating
statements with empty lines unless it's the only statement in that block.
* Decorate `break`, `continue`, `return`, and other terminating statements
with empty lines unless it's the only statement in that block.
* Don't group type declarations together. Unlike with blocks of `const`s,
where a `iota` may be used or where all constants belong to a certain type,

View File

@@ -34,11 +34,11 @@
'jobs':
- 'Publish to Snapstore'
- 'Publish to Github Releases':
- 'Publish to GitHub Releases':
'manual': false
'final': false
'jobs':
- 'Publish to Github Releases'
- 'Publish to GitHub Releases'
'Make release':
'docker':
@@ -194,7 +194,7 @@
'requirements':
- 'adg-docker': 'true'
'Publish to Github Releases':
'Publish to GitHub Releases':
'key': 'PTGR'
'other':
'clean-working-dir': true
@@ -215,7 +215,7 @@
export CHANNEL="${bamboo.channel}"
if [ "$CHANNEL" != 'release' ] && [ "${CHANNEL}" != 'beta' ]
then
echo "don't publish to Github Releases for this channel"
echo "don't publish to GitHub Releases for this channel"
exit 0
fi

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Новая статычная арэнда",
"dhcp_static_leases_not_found": "Не знойдзена статычных арэнд DHCP",
"dhcp_add_static_lease": "Дадаць статычную арэнду",
"dhcp_reset_leases": "Скінуць усё арэнды",
"dhcp_reset_leases_confirm": "Вы ўпэўнены, што хочаце выдаліць усё арэнды?",
"dhcp_reset_leases_success": "Арэнды DHCP паспяхова выдалены",
"dhcp_reset": "Вы ўпэўнены, што хочаце скінуць налады DHCP?",
"country": "Краіна",
"city": "Горад",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Уставіць змесціва зачыненага ключа",
"stats_params": "Канфігурацыя статыстыкі",
"config_successfully_saved": "Канфігурацыя паспяхова захавана",
"interval_6_hour": "6 гадзін",
"interval_24_hour": "24 гадзіны",
"interval_days": "{{count}} дзень",
"interval_days_plural": "{{count}} дзён",
"domain": "Дамен",
"punycode": "Punycode",
"answer": "Адказ",
"filter_added_successfully": "Спіс паспяхова дададзены",
"filter_removed_successfully": "Спіс паспяхова выдалены",

View File

@@ -68,6 +68,9 @@
"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_leases": "Resetovat všechny pronájmy",
"dhcp_reset_leases_confirm": "Opravdu chcete resetovat všechny pronájmy?",
"dhcp_reset_leases_success": "Pronájmy DHCP byli úspěšně resetovány",
"dhcp_reset": "Opravdu chcete resetovat konfiguraci DHCP?",
"country": "Země",
"city": "Město",
@@ -423,9 +426,9 @@
"access_title": "Nastavení přístupu",
"access_desc": "Zde můžete konfigurovat pravidla přístupu pro server DNS AdGuard Home.",
"access_allowed_title": "Povolení klienti",
"access_allowed_desc": "Seznam adres CIDR nebo IP. Pokud je nakonfigurován, AdGuard Home bude přijímat požadavky pouze z těchto IP adres.",
"access_allowed_desc": "Seznam CIDR, IP adres nebo ID klientů. Pokud je nakonfigurován, AdGuard Home bude přijímat požadavky pouze od těchto klientů.",
"access_disallowed_title": "Blokovaní klienti",
"access_disallowed_desc": "Seznam adres CIDR nebo IP. Pokud je nakonfigurován, AdGuard Home bude odmítat požadavky pouze z těchto IP adres.",
"access_disallowed_desc": "Seznam CIDR, IP adres nebo ID klientů. Pokud je nakonfigurován, AdGuard Home bude odmítat požadavky od těchto klientů. Pokud jsou povolení klienti nakonfigurováni, je toto pole ignorováno.",
"access_blocked_title": "Blokované domény",
"access_blocked_desc": "Nezaměňujte to s filtry. AdGuard Home zruší dotazy DNS odpovídající těmto doménám a tyto dotazy se neobjeví ani v protokolu dotazů. Zde můžete určit přesné názvy domén, zástupné znaky a pravidla filtrování URL adres, např. \"example.org\", \"*.example.org\" nebo \"||example.org^\".",
"access_settings_saved": "Nastavení přístupu bylo úspěšně uloženo",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Vložte obsahy soukromého klíče",
"stats_params": "Konfigurace statistik",
"config_successfully_saved": "Konfigurace byla úspěšně uložena",
"interval_6_hour": "6 hodin",
"interval_24_hour": "24 hodin",
"interval_days": "Dny: {{count}}",
"interval_days_plural": "Dny: {{count}}",
"domain": "Doména",
"punycode": "Punycode",
"answer": "Odpověď",
"filter_added_successfully": "Seznam byl úspěšně přidán",
"filter_removed_successfully": "Seznam byl úspěšně odstraněn",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Ny statisk lease",
"dhcp_static_leases_not_found": "Intet DHCP statisk leases fundet",
"dhcp_add_static_lease": "Tilføj statisk lease",
"dhcp_reset_leases": "Nulstil alle gyldighedsperioder",
"dhcp_reset_leases_confirm": "Sikker på, at du vil nulstille alle gyldighedsperioder?",
"dhcp_reset_leases_success": "DHCP- gyldighedsperioder nulstillet",
"dhcp_reset": "Sikker på, at du vil nulstille DHCP-opsætningen?",
"country": "Land",
"city": "By",
@@ -423,9 +426,9 @@
"access_title": "Adgangsindstillinger",
"access_desc": "Her kan du opsætte adgangsregler for AdGuard Home DNS-serveren.",
"access_allowed_title": "Tilladte klienter",
"access_allowed_desc": "En liste over CIDR- eller IP-adresser. Hvis opsat, accepterer AdGuard Home kun forespørgsler fra disse IP-adresser.",
"access_allowed_desc": "En liste over CIDR-, IP-adresser eller klient-ID'er. Hvis opsat, accepterer AdGuard Home kun forespørgsler fra disse klienter.",
"access_disallowed_title": "Ikke tilladte klienter",
"access_disallowed_desc": "En liste over CIDR- eller IP-adresser. Hvis opsat, dropper AdGuard Home forespørgsler fra disse IP-adresser.",
"access_disallowed_desc": "En liste over CIDR-, IP-adresser eller klient-ID'er. Hvis opsat, dropper AdGuard Home forespørgsler fra disse klienter. Opsættes tilladte klienter, ignoreres dette felt.",
"access_blocked_title": "Ikke tilladte domæner",
"access_blocked_desc": "Ikke at forveksle med filtre. AdGuard Home dropper DNS-forespørgsler matchende disse domæner, ej heller vil forespørgslerne optræde i forespørgselsloggen. Der kan angives præcise domænenavne, jokertegn eller URL-filterregler, f.eks. \"eksempel.org\", \"*.eksempel.org\", \"||eksempel.org^\" eller tilsvarende.",
"access_settings_saved": "Adgangsindstillinger gemt",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Indsæt indholdet af den private nøgle",
"stats_params": "Statistikopsætning",
"config_successfully_saved": "Opsætning er gemt",
"interval_6_hour": "6 timer",
"interval_24_hour": "24 timer",
"interval_days": "{{count}} dag",
"interval_days_plural": "{{count}} dage",
"domain": "Domæne",
"punycode": "Punycode",
"answer": "Svar",
"filter_added_successfully": "Listen er tilføjet",
"filter_removed_successfully": "Listen er blevet fjernet",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Neuer statischer Lease",
"dhcp_static_leases_not_found": "Keine statischen DHCP-Leases gefunden",
"dhcp_add_static_lease": "Statischen Lease hinzufügen",
"dhcp_reset_leases": "Setzen Sie alle Leases zurück",
"dhcp_reset_leases_confirm": "Möchten Sie wirklich alle Leases zurücksetzen?",
"dhcp_reset_leases_success": "DHCP-Leases erfolgreich zurückgesetzt",
"dhcp_reset": "Möchten Sie die DHCP-Konfiguration wirklich zurücksetzen?",
"country": "Land",
"city": "Stadt",
@@ -423,9 +426,9 @@
"access_title": "Zugriffsrechte",
"access_desc": "Hier können Sie die Zugriffsregeln für den AdGuard Home DNS-Server konfigurieren.",
"access_allowed_title": "Zugelassene Clients",
"access_allowed_desc": "Eine Liste von CIDR- oder IP-Adressen. Wenn konfiguriert, akzeptiert AdGuard Home nur Anfragen von diesen IP-Adressen.",
"access_allowed_desc": "Eine Liste von CIDRs, IP-Adressen oder Client-IDs. Wenn konfiguriert, akzeptiert AdGuard Home Anfragen von diesen Clients.",
"access_disallowed_title": "Nicht zugelassene Clients",
"access_disallowed_desc": "Eine Liste von CIDR- oder IP-Adressen. Wenn konfiguriert, löscht AdGuard Home Anfragen von diesen IP-Adressen.",
"access_disallowed_desc": "Eine Liste von CIDRs, IP-Adressen oder Client-IDs. Wenn konfiguriert, löscht AdGuard Home Anfragen von diesen Clients. Wenn erlaubte Clients konfiguriert sind, wird dieses Feld ignoriert.",
"access_blocked_title": "Nicht zugelassene Domains",
"access_blocked_desc": "Verwechseln Sie dies nicht mit Filtern. AdGuard Home verwirft DNS-Abfragen, die mit diesen Domänen übereinstimmen, und diese Abfragen erscheinen nicht einmal im Abfrageprotokoll. Hier können Sie die genauen Domain-Namen, Wildcards und URL-Filter-Regeln angeben, z.B. 'beispiel.org', '*.beispiel.org' oder '||beispiel.org^'.",
"access_settings_saved": "Zugriffseinstellungen erfolgreich gespeichert",
@@ -485,6 +488,7 @@
"interval_days": "{{count}} Tag",
"interval_days_plural": "{{count}} Tage",
"domain": "Domain",
"punycode": "Punycode",
"answer": "Antwort",
"filter_added_successfully": "Der Filter wurde erfolgreich hinzugefügt",
"filter_removed_successfully": "Der Filter wurde erfolgreich entfernt",

View File

@@ -68,6 +68,9 @@
"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_leases": "Reset all leases",
"dhcp_reset_leases_confirm": "Are you sure you want to reset all leases?",
"dhcp_reset_leases_success": "DHCP leases successfully reset",
"dhcp_reset": "Are you sure you want to reset the DHCP configuration?",
"country": "Country",
"city": "City",
@@ -423,9 +426,9 @@
"access_title": "Access settings",
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
"access_allowed_title": "Allowed clients",
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
"access_allowed_desc": "A list of CIDRs, IP addresses, or client IDs. If configured, AdGuard Home will accept requests only from these clients.",
"access_disallowed_title": "Disallowed clients",
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
"access_disallowed_desc": "A list of CIDRs, IP addresses, or client IDs. If configured, AdGuard Home will drop requests from these clients. If allowed clients are configured, this field is ignored.",
"access_blocked_title": "Disallowed domains",
"access_blocked_desc": "Not to be confused with filters. AdGuard Home drops DNS queries matching these domains, and these queries don't even appear in the query log. You can specify exact domain names, wildcards, or URL filter rules, e.g. \"example.org\", \"*.example.org\", or \"||example.org^\" correspondingly.",
"access_settings_saved": "Access settings successfully saved",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Paste the private key contents",
"stats_params": "Statistics configuration",
"config_successfully_saved": "Configuration successfully saved",
"interval_6_hour": "6 hours",
"interval_24_hour": "24 hours",
"interval_days": "{{count}} day",
"interval_days_plural": "{{count}} days",
"domain": "Domain",
"punycode": "Punycode",
"answer": "Answer",
"filter_added_successfully": "The list has been successfully added",
"filter_removed_successfully": "The list has been successfully removed",

View File

@@ -68,6 +68,9 @@
"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_leases": "Restablecer todas las asignaciones",
"dhcp_reset_leases_confirm": "¿Estás seguro de que deseas restablecer todas las asignaciones?",
"dhcp_reset_leases_success": "Asignaciones DHCP restablecidas correctamente",
"dhcp_reset": "¿Estás seguro de que deseas restablecer la configuración DHCP?",
"country": "País",
"city": "Ciudad",
@@ -423,9 +426,9 @@
"access_title": "Configuración de acceso",
"access_desc": "Aquí puedes configurar las reglas de acceso para el servidor DNS de AdGuard Home.",
"access_allowed_title": "Clientes permitidos",
"access_allowed_desc": "Lista de CIDR o direcciones IP. Si está configurado, AdGuard Home solo aceptará peticiones de estas direcciones IP.",
"access_allowed_desc": "Lista de CIDR, direcciones IP o ID de clientes. Si está configurado, AdGuard Home aceptará peticiones solo de estos clientes.",
"access_disallowed_title": "Clientes no permitidos",
"access_disallowed_desc": "Lista de CIDR o direcciones IP. Si está configurado, AdGuard Home descartará las peticiones de estas direcciones IP.",
"access_disallowed_desc": "Lista de CIDR, direcciones IP o ID de clientes. Si está configurado, AdGuard Home descartará las peticiones de estos clientes. Si se configuran clientes permitidos, este campo será ignorado.",
"access_blocked_title": "Dominios no permitidos",
"access_blocked_desc": "No debe confundirse con filtros. AdGuard Home descartará las consultas DNS que coincidan con estos dominios, y estas consultas ni siquiera aparecerán en el registro de consultas. Puedes especificar nombres de dominio exactos, comodines o reglas de filtrado de URL, por ejemplo: \"ejemplo.org\", \"*.ejemplo.org\" o \"||ejemplo.org^\" correspondientemente.",
"access_settings_saved": "Configuración de acceso guardado correctamente",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Pegar el contenido de la clave privada",
"stats_params": "Configuración de estadísticas",
"config_successfully_saved": "Configuración guardada correctamente",
"interval_6_hour": "6 horas",
"interval_24_hour": "24 horas",
"interval_days": "{{count}} día",
"interval_days_plural": "{{count}} días",
"domain": "Dominio",
"punycode": "Punycode",
"answer": "Respuesta",
"filter_added_successfully": "La lista ha sido añadida correctamente",
"filter_removed_successfully": "La lista ha sido eliminada correctamente",
@@ -544,7 +549,7 @@
"filtered_custom_rules": "Filtrado por reglas de filtrado personalizado",
"choose_from_list": "Elegir de la lista",
"add_custom_list": "Añadir lista personalizada",
"host_whitelisted": "El host está en la lista blanca",
"host_whitelisted": "El host está permitido",
"check_ip": "Direcciones IP: {{ip}}",
"check_cname": "CNAME: {{cname}}",
"check_reason": "Razón: {{reason}}",
@@ -569,7 +574,7 @@
"validated_with_dnssec": "Validado con DNSSEC",
"all_queries": "Todas las consultas",
"show_blocked_responses": "Bloqueado",
"show_whitelisted_responses": "En lista blanca",
"show_whitelisted_responses": "Permitido",
"show_processed_responses": "Procesado",
"blocked_safebrowsing": "Bloqueado por navegación segura",
"blocked_adult_websites": "Sitios web para adultos bloqueado",

View File

@@ -23,7 +23,7 @@
"disabled_dhcp": "Serveur DHCP désactivé",
"unavailable_dhcp": "Le DHCP nest pas disponible",
"unavailable_dhcp_desc": "AdGuard Home ne peut pas exécuter un serveur DHCP sur votre système dexploitation",
"dhcp_title": "Serveur DHCP (experimental !)",
"dhcp_title": "Serveur DHCP (expérimental !)",
"dhcp_description": "Si votre routeur ne fonctionne pas avec les réglages DHCP, vous pouvez utiliser le serveur DHCP par défaut d'AdGuard.",
"dhcp_enable": "Activer le serveur DHCP",
"dhcp_disable": "Désactiver le serveur DHCP",
@@ -57,7 +57,7 @@
"dhcp_hardware_address": "Adresse de la machine",
"dhcp_ip_addresses": "Adresses IP",
"ip": "IP",
"dhcp_table_hostname": "Nom de machine",
"dhcp_table_hostname": "Nom d'hôte",
"dhcp_table_expires": "Expire le",
"dhcp_warning": "Si vous souhaitez tout de même activer le serveur DHCP, assurez-vous qu'il n'y a pas d'autre serveur DHCP actif sur votre réseau. Sinon, cela peut perturber la connexion Internet sur tous les appareils connectés au réseau !",
"dhcp_error": "AdGuard Home ne peut pas déterminer s'il y a un autre serveur DHCP actif sur le réseau.",
@@ -91,7 +91,7 @@
"address": "Addresse",
"protocol": "Protocole",
"on": "Activé",
"off": "Éteint",
"off": "Désactivé",
"copyright": "Copyright",
"homepage": "Page d'accueil",
"report_an_issue": "Signaler un problème",
@@ -100,7 +100,7 @@
"enabled_protection": "Protection activée",
"disable_protection": "Désactiver la protection",
"disabled_protection": "Protection désactivée",
"refresh_statics": "Renouveler les statistiques",
"refresh_statics": "Actualiser les statistiques",
"dns_query": "Requêtes DNS",
"blocked_by": "<0>Bloqué par Filtres</0>",
"stats_malware_phishing": "Tentative de malware/hameçonnage bloquée",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Nuovo lease statico",
"dhcp_static_leases_not_found": "Non è stato trovato nessun leases statico DHCP",
"dhcp_add_static_lease": "Aggiungi lease statico",
"dhcp_reset_leases": "Reimposta tutti i temporanei",
"dhcp_reset_leases_confirm": "Sei sicuro di voler ripristinare tutti i temporanei?",
"dhcp_reset_leases_success": "DHCP temporanei reimpostati correttamente",
"dhcp_reset": "Sei sicuro di voler ripristinare la configurazione DHCP?",
"country": "Regione",
"city": "Città",
@@ -423,9 +426,9 @@
"access_title": "Impostazioni di accesso",
"access_desc": "Qui puoi configurare le regole d'accesso per il server DNS di AdGuard Home.",
"access_allowed_title": "Client permessi",
"access_allowed_desc": "Una lista in CIDR o indirizzi IP. Se configurata AdGuard Home accetterà richieste solo da questi indirizzi ip.",
"access_allowed_desc": "Una lista di CIDR, indirizzi IP o ID client. Se configurata AdGuard Home accetterà richieste solo da questi client.",
"access_disallowed_title": "Client non permessi",
"access_disallowed_desc": "Una lista in CIDR o indirizzi IP. Se configurata AdGuard Home non accetterà richieste da questi indirizzi ip.",
"access_disallowed_desc": "Una lista di CIDR, indirizzi IP o ID client. Se configurata, AdGuard Home rifiuterà richieste da questi client. Se i client consentiti risulteranno configurati, questo campo verrà ignorato.",
"access_blocked_title": "Domini bloccati",
"access_blocked_desc": "Da non confondere con i filtri. AdGuard Home eliminerà le richieste DNS corrispondenti a questi domini e queste richieste non verranno visualizzate nel relativo registro. Puoi specificare nomi di dominio esatti, caratteri jolly o regole di filtraggio URL, ad esempio \"esempio.org\", \"*.esempio.org\" o \"||esempio.org^\".",
"access_settings_saved": "Impostazioni di accesso salvate correttamente",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Incolla i contenuti della chiave privata",
"stats_params": "Configurazione delle statistiche",
"config_successfully_saved": "Configurazione salvata correttamente",
"interval_6_hour": "6 ore",
"interval_24_hour": "24 ore",
"interval_days": "{{count}} giorni",
"interval_days_plural": "{{count}} giorni",
"domain": "Dominio",
"punycode": "Punycode",
"answer": "Risposta",
"filter_added_successfully": "Il filtro è stato aggiunto correttamente",
"filter_removed_successfully": "La lista è stata correttamente rimossa",

View File

@@ -5,16 +5,18 @@
"upstream_parallel": "並列リクエストを使用する(同時にすべてのアップストリームサーバーに処理要求することで解決スピードが向上)",
"parallel_requests": "並列リクエスト",
"load_balancing": "ロードバランシング",
"load_balancing_desc": "一度に1つのサーバに処理要求します。 AdGuard Homeは、重み付きランダムアルゴリズムweighted random algorithmを使用してサーバを選択するため、最速のサーバがより頻繁に使用されます。",
"load_balancing_desc": "一度に1つのアップストリームサーバに処理要求します。 AdGuard Homeは、重み付きランダムアルゴリズムweighted random algorithmを使用してサーバを選択するため、最速のサーバがより頻繁に使用されます。",
"bootstrap_dns": "ブートストラップDNSサーバ",
"bootstrap_dns_desc": "ブートストラップDNSサーバは、上流として指定したDoHDoTリゾルバのIPアドレスを解決するために使用されます。",
"local_ptr_title": "プライベートリバースDNSサーバー",
"local_ptr_desc": "AdGuard HomeがローカルPTRクエリに使用するDNSサーバーです。これらのサーバーは、rDNSを使ってプライベートIPアドレス例えば\"192.168.12.34\"を持つクライアントのホスト名を解決するために使用されます。設定されていない場合、AdGuard HomeはOSのデフォルトDNSリゾルバーを自動的に使用します。",
"local_ptr_desc": "AdGuard HomeがローカルPTRクエリに使用するDNSサーバーです。これらのサーバーは、rDNSを使ってプライベートIPアドレス例えば\"192.168.12.34\"を持つクライアントのホスト名を解決するために使用されます。設定されていない場合、AdGuard HomeはOSのデフォルトDNSリゾルバーのアドレスAdGuard Home自体のアドレスを除くを自動的に使用します。",
"local_ptr_default_resolver": "デフォルトでは、AdGuard Homeは次のリバースDNSリゾルバを使用します: {{ip}}",
"local_ptr_no_default_resolver": "AdGuard Homeは、このシステムに適したプライベートリバースDNSリゾルバを特定できませんでした。",
"local_ptr_placeholder": "1行に1つのサーバを入力してください。",
"resolve_clients_title": "クライアントのIPアドレスの逆解決を有効にする",
"resolve_clients_desc": "有効にすると、AdGuard Homeは、対応するリゾルバーローカルクライアントの場合はプライベートDNSサーバ、パブリックIPを持つクライアントの場合は上流サーバにPTRクエリを送信することにより、クライアントのIPアドレスをホストに逆解決しようとします。",
"use_private_ptr_resolvers_title": "プライベートrDNSリゾルバを使用",
"use_private_ptr_resolvers_desc": "これらの上流サーバを使用して、ローカルで提供されるアドレスのリバースDNSルックアップを実行します。無効にすると、AdGuard Homeは、DHCP, /etc/hosts などから認識されるクライアントを除、すべてのPTR要求にNXDOMAINで応答します。",
"resolve_clients_desc": "対応するリゾルバーローカルクライアントの場合はプライベートDNSサーバ、パブリックIPを持つクライアントの場合は上流サーバにPTRクエリを送信することにより、クライアントのIPアドレスをホストネームに逆解決します。",
"use_private_ptr_resolvers_title": "プライベートリバースDNSリゾルバを使用",
"use_private_ptr_resolvers_desc": "これらの上流サーバを使用して、ローカルで提供されるアドレスのリバースDNSルックアップを実行します。無効にすると、AdGuard Homeは、DHCP, /etc/hosts などから認識されるクライアントを除、すべてのこのようなPTR要求にNXDOMAINで応答します。",
"check_dhcp_servers": "DHCPサーバをチェックする",
"save_config": "構成を保存する",
"enabled_dhcp": "DHCPサーバを有効にしました",
@@ -66,6 +68,9 @@
"dhcp_new_static_lease": "新規静的割り当て",
"dhcp_static_leases_not_found": "DHCP静的割り当てはありません",
"dhcp_add_static_lease": "静的割り当てを追加する",
"dhcp_reset_leases": "すべてのリースをリセットする",
"dhcp_reset_leases_confirm": "すべてのリース(割り当て)をリセットしてもよろしいですか?",
"dhcp_reset_leases_success": "すべてのDHCPリース割り当てがリセット完了しました。",
"dhcp_reset": "DHCP構成をリセットしてよろしいですか",
"country": "国",
"city": "街",
@@ -126,11 +131,11 @@
"block_domain_use_filters_and_hosts": "フィルタとhostsファイルを使用してドメインをブロックする",
"filters_block_toggle_hint": "<a>フィルタ</a>の設定でブロックするルールを設定することができます。",
"use_adguard_browsing_sec": "AdGuardブラウジングセキュリティ・ウェブサービスを使用する",
"use_adguard_browsing_sec_hint": "AdGuard Homeは、ブラウジングセキュリティ・ウェブサービスによってドメインがブックリストに登録されているかどうかを確認します。 これはプライバシーを考慮したAPIを使用してチェックを実行しますドメイン名SHA256ハッシュの短いプレフィックスのみがサーバに送信されます。",
"use_adguard_browsing_sec_hint": "AdGuard Homeは、ブラウジングセキュリティ・ウェブサービスによってドメインがブックされているかを確認します。 確認は、プライバシーに配慮したルックアップAPIを使用して行いますドメイン名SHA256ハッシュの短いプレフィックスのみがサーバに送信されます。",
"use_adguard_parental": "AdGuardペアレンタルコントロール・ウェブサービスを使用する",
"use_adguard_parental_hint": "AdGuard Homeは、ドメインにアダルトコンテンツが含まれているかどうかを確認します。 ブラウジングセキュリティ・ウェブサービスと同じプライバシーに優しいAPIを使用します。",
"enforce_safe_search": "セーフサーチを強制する",
"enforce_save_search_hint": "AdGuard Homeは、 Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay という検索エンジンでセーフサーチを強制適用できます。",
"enforce_save_search_hint": "AdGuard Homeは、次の検索エンジンでセーフサーチを強制適用します: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay",
"no_servers_specified": "サーバが指定されていません",
"general_settings": "一般設定",
"dns_settings": "DNS設定",
@@ -269,7 +274,7 @@
"plain_dns": "通常のDNS",
"form_enter_rate_limit": "頻度制限を入力してください",
"rate_limit": "頻度制限",
"edns_enable": "EDNSクライアントサブネットを有効にする",
"edns_enable": "EDNSクライアントサブネットを有効にする",
"edns_cs_desc": "有効にすると、AdGuard HomeはクライアントのサブネットをDNSサーバへ送信します。",
"rate_limit_desc": "一つのクライアントに対して許可される1秒あたりのリクエスト数「0」に設定すると、制限なしになります",
"blocking_ipv4_desc": "ブロックされたAリクエストに対して応答されるIPアドレス",
@@ -421,11 +426,11 @@
"access_title": "アクセス設定",
"access_desc": "ここで、AdGuard Home DNSサーバのアクセスルールを設定できます。",
"access_allowed_title": "許可されたクライアント",
"access_allowed_desc": "CIDRまたはIPアドレスのリスト。設定されると、AdGuard HomeはこれらのIPアドレスからのリクエストのみを許可します。",
"access_allowed_desc": "CIDRIPアドレス、またはクライアントIDのリスト。設定されている場合、AdGuard HomeはこれらのIPアドレスからのリクエストのみを受け入れます。",
"access_disallowed_title": "拒否するクライアント",
"access_disallowed_desc": "CIDRまたはIPアドレスのリスト。設定されると、AdGuard HomeはこれらのIPアドレスからのリクエストを破棄します。",
"access_disallowed_desc": "CIDRIPアドレス、またはクライアントIDのリスト。設定されている場合、AdGuard HomeはこれらのIPアドレスからのリクエストを破棄します。「許可されたクライアント」欄が設定されている場合、この欄は無視されます。",
"access_blocked_title": "拒否するドメイン",
"access_blocked_desc": "こちらをフィルタと混同しないでください。AdGuard Homeは、クエリクエスチョンにこれらのドメインを含むDNSクエリをドロップします。ここでは、「example.org」、「*.example.org」、「 ||example.org^ 」など、特定のドメイン名、ワイルドカード、URLフィルタルールを指定できます。",
"access_blocked_desc": "こちらをフィルタと混同しないでください。AdGuard Homeは、ここで入力されたドメインに一致するDNSクエリをドロップし、そういったクエリはクエリログにも表示されません。ここでは、「example.org」、「*.example.org」、「 ||example.org^ 」など、特定のドメイン名、ワイルドカード、URLフィルタルールを入力できます。",
"access_settings_saved": "アクセス設定の保存に成功しました",
"updates_checked": "アップデートの確認に成功しました",
"updates_version_equal": "AdGuard Homeは既に最新です",
@@ -479,10 +484,12 @@
"encryption_key_source_content": "秘密鍵の内容をペーストする",
"stats_params": "統計設定",
"config_successfully_saved": "設定の保存に成功しました",
"interval_6_hour": "6時間",
"interval_24_hour": "24時間",
"interval_days": "{{count}}日",
"interval_days_plural": "{{count}}日",
"domain": "ドメイン",
"punycode": "Punycode",
"answer": "応答",
"filter_added_successfully": "フィルタの追加に成功しました",
"filter_removed_successfully": "リストの削除に成功しました。",
@@ -525,8 +532,8 @@
"rewrite_domain_name": "ドメイン名入力した場合CNAME記録が追加されます。",
"rewrite_A": "<0>A</0>:特別な値、アップストリームからの<0>A</0>記録を保持します。",
"rewrite_AAAA": "<0>AAAA</0>:特別な値、アップストリームからの<0>AAAA</0>記録を保持します。",
"disable_ipv6": "IPv6を無効にする",
"disable_ipv6_desc": "チェックすると、IPv6アドレスタイプAAAAすべてのDNSクエリは破棄されます。",
"disable_ipv6": "IPv6アドレスの解決を無効にする",
"disable_ipv6_desc": "IPv6アドレスタイプAAAAに対するすべてのDNSクエリをドロップします。",
"fastest_addr": "最速のIPアドレス",
"fastest_addr_desc": "すべてのDNSサーバーに処理要求し、全応答の中で最速のIPアドレスを返します。これにより、AdGuard HomeがすべてのDNSサーバーからの応答を待つ必要があるため、DNSクエリが遅くなりますが、全体的な接続性は向上します。",
"autofix_warning_text": "\"改善\"をクリックすると、AdGuardHomeはAdGuardHome DNSサーバを使用するようにシステムを構成します。",
@@ -563,7 +570,7 @@
"list_updated": "{{count}}個のリストが更新されました",
"list_updated_plural": "{{count}}個のリストが更新されました",
"dnssec_enable": "DNSSECを有効にする",
"dnssec_enable_desc": "DNSクエリの応答にDNSSECフラグを設定し、結果を確認しますDNSSEC対応リゾルバが必要です)",
"dnssec_enable_desc": "送信するDNSクエリにDNSSECフラグを設定し、結果を確認しますDNSSEC対応リゾルバが必要です",
"validated_with_dnssec": "DNSSECにて検証済",
"all_queries": "すべてのクエリ",
"show_blocked_responses": "ブロック済",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "새 고정 임대",
"dhcp_static_leases_not_found": "DHCP 고정 임대를 찾을 수 없음",
"dhcp_add_static_lease": "고정 임대 추가",
"dhcp_reset_leases": "모든 임대 초기화",
"dhcp_reset_leases_confirm": "정말로 모든 임대를 초기화할까요?",
"dhcp_reset_leases_success": "DHCP 임대 성공적으로 초기화됨",
"dhcp_reset": "정말로 DHCP 설정을 초기화할까요?",
"country": "지역",
"city": "도시",
@@ -423,9 +426,9 @@
"access_title": "접근 설정",
"access_desc": "여기에서 AdGuard Home DNS 서버에 대한 액세스 규칙을 구성할 수 있습니다.",
"access_allowed_title": "허용된 클라이언트",
"access_allowed_desc": "CIDR 또는 IP 주소 목록입니다. 구성된 경우 AdGuard Home은 이러한 IP 주소의 요청만 수락할 수 있습니다.",
"access_allowed_desc": "CIDR, IP 주소 또는 클라이언트 ID 목록입니다. 허용된 클라이언트가 구성된 경우, AdGuard Home은 이 클라이언트의 요청만 수락니다.",
"access_disallowed_title": "차단된 클라이언트",
"access_disallowed_desc": "CIDR 또는 IP 주소 목록입니다. 구성된 경우 AdGuard Home은 이러한 IP 주소의 요청을 삭제합니다.",
"access_disallowed_desc": "CIDR, IP 주소 또는 클라이언트 ID 목록입니다. 차단된 클라이언트가 구성된 경우, AdGuard Home은 이 클라이언트의 요청을 무시합니다. 허용된 클라이언트가 구성된 경우, 이 필드는 무시됩니다.",
"access_blocked_title": "차단된 도메인",
"access_blocked_desc": "이 기능을 필터와 혼동하지 마세요. AdGuard Home은 이 도메인에 대한 DNS 요청을 무시합니다. 여기에서는 'example.org' '*. example.org', '|| example.org ^'와 같은 특정 도메인 이름, 와일드 카드, URL 필터 규칙을 지정할 수 있습니다.",
"access_settings_saved": "액세스 설정이 성공적으로 저장되었습니다.",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "비밀키 내용 붙여넣기",
"stats_params": "통계 구성",
"config_successfully_saved": "설정이 성공적으로 저장되었습니다.",
"interval_6_hour": "6시간",
"interval_24_hour": "24시간",
"interval_days": "{{count}} 일",
"interval_days_plural": "{{count}} 일",
"domain": "도메인",
"punycode": "Punycode",
"answer": "응답",
"filter_added_successfully": "목록이 성공적으로 추가됨",
"filter_removed_successfully": "목록이 성공적으로 제거되었습니다",

View File

@@ -68,6 +68,9 @@
"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_leases": "Alle leases resetten",
"dhcp_reset_leases_confirm": "Weet je zeker dat je alle leases wilt resetten?",
"dhcp_reset_leases_success": "DHCP-leases succesvol gereset",
"dhcp_reset": "Weet je zeker dat je de DHCP configuratie wil resetten?",
"country": "Land",
"city": "Stad",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Nowa dzierżawa statyczna",
"dhcp_static_leases_not_found": "Nie znaleziono statycznych dzierżaw DHCP",
"dhcp_add_static_lease": "Dodaj dzierżawę statyczną",
"dhcp_reset_leases": "Zresetuj wszystkie dzierżawy",
"dhcp_reset_leases_confirm": "Czy na pewno chcesz zresetować wszystkie dzierżawy?",
"dhcp_reset_leases_success": "Pomyślnie zresetowano dzierżawy DHCP",
"dhcp_reset": "Czy na pewno chcesz zresetować konfigurację DHCP?",
"country": "Kraj",
"city": "Miasto",
@@ -423,9 +426,9 @@
"access_title": "Ustawienia dostępu",
"access_desc": "Tutaj możesz skonfigurować reguły dostępu dla serwera DNS AdGuard Home.",
"access_allowed_title": "Dozwoleni klienci",
"access_allowed_desc": "Lista adresów CIDR lub IP. Jeśli jest skonfigurowany, AdGuard Home akceptuje tylko żądania z tych adresów IP.",
"access_allowed_desc": "Lista CIDR-ów, adresów IP lub identyfikatorów klientów. Jeśli zostanie skonfigurowana, AdGuard Home będzie przyjmował żądania tylko od tych klientów.",
"access_disallowed_title": "Niedozwoleni klienci",
"access_disallowed_desc": "Lista adresów CIDR lub IP. Po skonfigurowaniu AdGuard Home usunie żądania z tych adresów IP.",
"access_disallowed_desc": "Lista CIDR-ów, adresów IP lub identyfikatorów klientów. Jeśli jest skonfigurowana, AdGuard Home będzie odrzucał żądania od tych klientów. Jeśli skonfigurowano dozwolonych klientów, pole to jest ignorowane.",
"access_blocked_title": "Niedozwolone domeny",
"access_blocked_desc": "Nie należy ich mylić z filtrami. AdGuard Home usuwa zapytania DNS pasujące do tych domen, a zapytania te nie pojawiają się nawet w dzienniku zapytań. Możesz określić dokładne nazwy domen, symbole wieloznaczne lub reguły filtrowania adresów URL, np. \"example.org\", \"*.example.org\" lub \"||example.org^\".",
"access_settings_saved": "Ustawienia dostępu zostały pomyślnie zapisane",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Wklej zawartość klucza prywatnego",
"stats_params": "Konfiguracja statystyk",
"config_successfully_saved": "Konfiguracja została pomyślnie zapisana",
"interval_6_hour": "6 godzin",
"interval_24_hour": "24 godziny",
"interval_days": "{{count}} dni",
"interval_days_plural": "{{count}} dni",
"domain": "Domena",
"punycode": "Punycode",
"answer": "Odpowiedź",
"filter_added_successfully": "Lista została pomyślnie dodana",
"filter_removed_successfully": "Lista została usunięta",

View File

@@ -68,6 +68,9 @@
"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_leases": "Redefinir todas as concessões",
"dhcp_reset_leases_confirm": "Tem certeza de que deseja redefinir todas as concessões?",
"dhcp_reset_leases_success": "Concessões de DHCP redefinidas com sucesso",
"dhcp_reset": "Você tem certeza de que deseja redefinir a configuração DHCP?",
"country": "País",
"city": "Cidade",
@@ -423,9 +426,9 @@
"access_title": "Configurações de acessos",
"access_desc": "Aqui você pode configurar as regras de acesso para o servidores de DNS do AdGuard Home.",
"access_allowed_title": "Clientes permitidos",
"access_allowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá permitir solicitações apenas desses endereços de IP.",
"access_allowed_desc": "Uma lista de CIDRs, endereços IP ou IDs de cliente. Se configurado, o AdGuard Home do aceitará solicitações apenas desses clientes.",
"access_disallowed_title": "Clientes não permitidos",
"access_disallowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá descartar as solicitações desses endereços de IP.",
"access_disallowed_desc": "Uma lista de CIDRs, endereços IP ou IDs de cliente. Se configurado, o AdGuard Home descartará as solicitações desses clientes. Se clientes permitidos estiverem configurados, este campo será ignorado.",
"access_blocked_title": "Domínios bloqueados",
"access_blocked_desc": "Não deve ser confundido com filtros. O AdGuard Home elimina as consultas DNS que correspondem a esses domínios, e essas consultas nem aparecem no registro de consultas. Você pode especificar nomes de domínio exatos, caracteres curinga ou regras de filtro de URL, por exemplo \"exemplo.org\", \"*.exemplo.org\", ou \"||exemplo.org^\" correspondentemente.",
"access_settings_saved": "Configurações de acesso foram salvas com sucesso",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Colar o conteúdo da chave privada",
"stats_params": "Configuração de estatísticas",
"config_successfully_saved": "Configuração salva com sucesso",
"interval_6_hour": "6 horas",
"interval_24_hour": "24 horas",
"interval_days": "{{count}} dias",
"interval_days_plural": "{{count}} dias",
"domain": "Domínio",
"punycode": "Punycode",
"answer": "Resposta",
"filter_added_successfully": "O filtro foi adicionado com sucesso",
"filter_removed_successfully": "A lista foi removida com sucesso",

View File

@@ -68,6 +68,9 @@
"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_leases": "Repor todas as concessões",
"dhcp_reset_leases_confirm": "Tem certeza de que deseja repor todas as concessões?",
"dhcp_reset_leases_success": "Concessões de DHCP repostas com sucesso",
"dhcp_reset": "Tem a certeza de que deseja repor a definição de DHCP?",
"country": "País",
"city": "Cidade",
@@ -423,9 +426,9 @@
"access_title": "Definições de acesso",
"access_desc": "Aqui pode configurar as regras de acesso para o servidores de DNS do AdGuard Home.",
"access_allowed_title": "Clientes permitidos",
"access_allowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá permitir solicitações apenas desses endereços de IP.",
"access_allowed_desc": "Uma lista de CIDRs, endereços IP ou IDs de cliente. Se configurado, o AdGuard Home do aceitará solicitações apenas desses clientes.",
"access_disallowed_title": "Clientes não permitidos",
"access_disallowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá descartar as solicitações desses endereços de IP.",
"access_disallowed_desc": "Uma lista de CIDRs, endereços IP ou IDs de cliente. Se configurado, o AdGuard Home descartará as solicitações desses clientes. Se clientes permitidos estiverem configurados, este campo será ignorado.",
"access_blocked_title": "Domínios bloqueados",
"access_blocked_desc": "Não deve ser confundido com filtros. O AdGuard Home elimina as consultas DNS que correspondem a esses domínios, e essas consultas nem aparecem no registro de consultas. Você pode especificar nomes de domínio exatos, caracteres curinga ou regras de filtro de URL, por exemplo \"exemplo.org\", \"*.exemplo.org\", ou \"||exemplo.org^\" correspondentemente.",
"access_settings_saved": "Definições de acesso foram guardadas com sucesso",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Colar o conteúdo da chave privada",
"stats_params": "Definição de estatísticas",
"config_successfully_saved": "Definição guardada com sucesso",
"interval_6_hour": "6 horas",
"interval_24_hour": "24 horas",
"interval_days": "{{count}} dias",
"interval_days_plural": "{{count}} dias",
"domain": "Domínio",
"punycode": "Punycode",
"answer": "Resposta",
"filter_added_successfully": "O filtro foi adicionado com sucesso",
"filter_removed_successfully": "A lista foi removida com sucesso",

View File

@@ -1,10 +1,24 @@
{
"client_settings": "Setări client",
"example_upstream_reserved": "Puteți specifica un DNS în amonte <0>pentru domeniul (domeniile) specific(e)</0>",
"example_upstream_comment": "Puteți specifica un comentariu",
"upstream_parallel": "Folosiți interogări paralele pentru a accelera rezolvarea, interogând simultan toate serverele în amonte.",
"parallel_requests": "Solicitări paralele",
"load_balancing": "Echilibrare-sarcini",
"load_balancing_desc": "Interoghează câte un server în amonte la un moment dat. AdGuard Home utilizează un algoritm de randomizare ponderat pentru a alege serverul, astfel încât cel mai rapid server să fie utilizat mai des.",
"bootstrap_dns": "Serverele DNS Bootstrap",
"bootstrap_dns_desc": "Serverele DNS Bootstrap sunt folosite pentru a rezolva adresele IP ale resolverelor DoH/DoT indicate ca upstreams.",
"local_ptr_title": "Servere DNS inverse private",
"local_ptr_desc": "Servere DNS pe care AdGuard Home le utilizează pentru interogări PTR locale. Aceste servere sunt folosite pentru a rezolva numele gazdelor de clienți cu adrese IP private, cum ar fi \"192.168.12.34\", folosind DNS inversat. Dacă nu este setat, AdGuard Home utilizează adresele resolverelor DNS implicite ale SO al dvs., cu excepția adreselor AdGuard Home înseși.",
"local_ptr_default_resolver": "În mod implicit, AdGuard Home utilizează următoarele resolvere DNS inverse: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home nu a putut determina resolvere DNS private adecvate pentru acest sistem.",
"local_ptr_placeholder": "Introduceți o adresă de server per linie",
"resolve_clients_title": "Permiteți rezolvarea inversa a adreselor IP ale clienților",
"resolve_clients_desc": "Rezolvă invers adresele IP ale clienților în numele lor de gazde prin trimiterea interogărilor PTR la resolverele corespunzătoare (servere DNS private pentru clienți locali, servere în amonte pentru clienți cu adrese IP publice).",
"use_private_ptr_resolvers_title": "Utilizați resolvere DNS inverse private",
"use_private_ptr_resolvers_desc": "Efectuează examinări DNS inverse pentru adresele deservite local folosind aceste servere în amonte. Dacă este dezactivată, AdGuard Home răspunde cu NXDOMAIN la toate aceste cereri PTR, cu excepția clienților cunoscuți din DHCP, /etc/hosts și așa mai departe.",
"check_dhcp_servers": "Căutați servere DHCP",
"save_config": "Salvare configurare",
"enabled_dhcp": "Server DHCP activat",
"disabled_dhcp": "Server DHCP dezactivat",
"unavailable_dhcp": "DHCP este indisponibil",
@@ -13,10 +27,12 @@
"dhcp_description": "Dacă routerul dvs. nu furnizează setări DHCP, puteți utiliza serverul DHCP încorporat AdGuard.",
"dhcp_enable": "Activați serverul DHCP",
"dhcp_disable": "Dezactivați serverul DHCP",
"dhcp_not_found": "Este sigur să activați serverul DHCP încorporat deoarece AdGuard Home nu a găsit niciun server DHCP activ în rețea. Cu toate acestea, ar trebui să verificați din nou manual, deoarece sondarea automată nu oferă în prezent o garanție de 100%.",
"dhcp_found": "În rețea se găsește un server DHCP activ. Nu este sigur să activați serverul DHCP încorporat.",
"dhcp_leases": "DHCP închiriate",
"dhcp_static_leases": "DHCP statice închiriate",
"dhcp_leases_not_found": "Nu s-au găsit DHCP închiriate",
"dhcp_config_saved": "Configurare DHCP salvată cu succes",
"dhcp_ipv4_settings": "Setări DHCP IPv4",
"dhcp_ipv6_settings": "Setări DHCP IPv6",
"form_error_required": "Câmp necesar",
@@ -26,6 +42,7 @@
"form_error_mac_format": "Format MAC invalid",
"form_error_client_id_format": "Format ID de client invalid",
"form_error_server_name": "Nume de server nevalid",
"form_error_subnet": "Subrețeaua „{{cidr}}” nu conține adresa IP „{{ip}}”",
"form_error_positive": "Trebuie să fie mai mare de 0",
"form_error_negative": "Trebuie să fie egală cu 0 sau mai mare",
"range_end_error": "Trebuie să fie mai mare decât începutul intervalului",
@@ -42,11 +59,19 @@
"ip": "IP",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expiră",
"dhcp_warning": "Dacă doriți să activați serverul DHCP oricum, asigurați-vă că nu există nici un alt server DHCP activ în rețeaua dvs., deoarece acest lucru poate rupe conectivitatea la Internet a dispozitivelor din rețea!",
"dhcp_error": "AdGuard Home nu a putut determina dacă există un alt server DHCP activ în rețea.",
"dhcp_static_ip_error": "Pentru a utiliza serverul DHCP, trebuie setată o adresă IP statică. AdGuard Home nu a reușit să determine dacă această interfață de rețea este configurată utilizând o adresă IP statică. Setați manual o adresă IP statică.",
"dhcp_dynamic_ip_found": "Sistemul dvs. folosește configurația dinamică a adreselor IP pentru interfața <0>{{interfaceName}}</0>. Pentru a utiliza serverul DHCP, trebuie setată o adresă IP statică. Adresa IP curentă este <0>{{ipAddress}}</0>. AdGuard Home o va configura automat ca adresă IP statică, dacă apăsați butonul \"Activați serverul DHCP\".",
"dhcp_lease_added": "\"{{key}}\" statică închiriată adăugată cu succes",
"dhcp_lease_deleted": "\"{{key}}\" statică închiriată eliminată cu succes",
"dhcp_new_static_lease": "Închiriere statică nouă",
"dhcp_static_leases_not_found": "Nu s-au găsit închirieri statice DHCP",
"dhcp_add_static_lease": "Adăugați închiriere statică",
"dhcp_reset_leases": "Resetați toate închirierile",
"dhcp_reset_leases_confirm": "Sigur doriți să resetați toate închirierile?",
"dhcp_reset_leases_success": "Închirierile DHCP au fost resetate cu succes",
"dhcp_reset": "Sigur doriți să resetați configurația DHCP?",
"country": "Țara",
"city": "Oraș",
"delete_confirm": "Sunteți sigur că doriți să ștergeți \"{{key}}\"?",
@@ -93,17 +118,24 @@
"top_clients": "Clienți de top",
"no_clients_found": "Nu au fost găsiți clienți",
"general_statistics": "Statistici generale",
"number_of_dns_query_days": "Numărul de interogări DNS procesate în ultima {{count}} zi",
"number_of_dns_query_days_plural": "Numărul de interogări DNS procesate în ultimele {{count}} zile",
"number_of_dns_query_24_hours": "Numărul de interogări DNS procesate în ultimele 24 de ore",
"number_of_dns_query_blocked_24_hours": "Numărul de interogări DNS blocate de filtrele adblock și lista de blocări din hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Numărul de interogări DNS blocate de modulul de securitate de navigare AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Numărul de site-uri pentru adulți blocate",
"enforced_save_search": "Căutare protejată întărită",
"number_of_dns_query_to_safe_search": "Numărul de interogări DNS pe motoarele de căutare pentru care a fost impusă Căutarea Sigură",
"average_processing_time": "Timpul mediu de procesare",
"average_processing_time_hint": "Timp mediu în milisecunde la procesarea unei cereri DNS",
"block_domain_use_filters_and_hosts": "Blocați domenii folosind filtre și fișiere hosts",
"filters_block_toggle_hint": "Puteți configura regulile de blocare în setările <a>Filtre</a>.",
"use_adguard_browsing_sec": "Utilizați serviciul Navigarea în Securitate AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home va verifica dacă domeniul este în lista de blocări a serviciul web de securitate de navigare. Pentru acesta va utiliza un lookup API discret: un prefix scurt al numelui de domeniu SHA256 hash este trimis serverului.",
"use_adguard_browsing_sec_hint": "AdGuard Home va verifica dacă domeniul este blocat de serviciul web de securitate de navigare. Pentru acesta, va utiliza un API de căutare discret: numai un prefix scurt al hash-ului SHA256 al numelui de domeniu este trimis la server.",
"use_adguard_parental": "Utilizați Controlul Parental AdGuard",
"use_adguard_parental_hint": "AdGuard Home va verifica pentru conținut adult pe domeniu. Utilizează același API discret ca cel utilizat de serviciul de securitate de navigare.",
"enforce_safe_search": "Căutare protejată întărită",
"enforce_save_search_hint": "AdGuard Home poate impune căutarea protejată în următoarele motoare de căutare: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.",
"enforce_safe_search": "Folosiți Căutarea Sigură",
"enforce_save_search_hint": "AdGuard Home va impune Căutarea Sigură în următoarele motoare de căutare: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Nu sunt specificate servere",
"general_settings": "Setări Generale",
"dns_settings": "Setări DNS",
@@ -115,6 +147,7 @@
"encryption_settings": "Setări de criptare",
"dhcp_settings": "Setări DHCP",
"upstream_dns": "Servere DNS în amonte",
"upstream_dns_help": "Introduceți o adresă de server pe linie. <a>Aflați mai multe</a> despre configurarea serverelor DNS în amonte.",
"upstream_dns_configured_in_file": "Configurat în {{path}}",
"test_upstream_btn": "Testați upstreams",
"upstreams": "Upstreams",
@@ -241,8 +274,9 @@
"plain_dns": "DNS simplu",
"form_enter_rate_limit": "Introduceți limita ratei",
"rate_limit": "Limita ratei",
"edns_enable": "Activați clientul subnet EDNS",
"edns_cs_desc": "Dacă este activat, AdGuard Home va trimite subnet-ul clienților către serverele DNS.",
"edns_enable": "Activați subrețeaua de clienți EDNS",
"edns_cs_desc": "Trimite subrețelele clienților la serverele DNS.",
"rate_limit_desc": "Numărul de interogări pe secundă permise pe client. Setarea la 0 înseamnă că nu există limită.",
"blocking_ipv4_desc": "Adresa IP de returnat pentru o cerere A de blocare",
"blocking_ipv6_desc": "Adresa IP de returnat pentru o cerere AAAA de blocare",
"blocking_mode_default": "Implicit: Răspunde cu adresa IP (0.0.0.0 for A; :: pentru AAAA) când sunt blocate de regulă tip Adblock; răspunde cu adresa IP specificată în regulă când sunt blocate de regula tip /etc/hosts",
@@ -265,6 +299,7 @@
"install_settings_listen": "Interfață de ascultare",
"install_settings_port": "Port",
"install_settings_interface_link": "Interfața dvs. de administrare AdGuard Home va fi disponibilă pe următoarele adrese:",
"form_error_port": "Introduceți un număr de port valid",
"install_settings_dns": "Server DNS",
"install_settings_dns_desc": "Va trebui să configurați aparatele sau routerul pentru a utiliza serverul DNS pe următoarele adrese:",
"install_settings_all_interfaces": "Toate interfețele",
@@ -279,12 +314,14 @@
"install_devices_title": "Configurați aparatele dvs",
"install_devices_desc": "Pentru a începe să utilizați AdGuard Home, trebuie să configurați aparatele.",
"install_submit_title": "Felicitări!",
"install_submit_desc": "Etapa de instalare este terminată și sunteți gata să începeți utilizarea AdGuard Home.",
"install_submit_desc": "Procedura de configurare este finalizată și acum sunteți gata să începeți utilizați AdGuard Home.",
"install_devices_router": "Router",
"install_devices_router_desc": "Această configurație va acoperi automat toate aparatele conectate la routerul de acasă și nu va trebui să le configurați manual pe fiecare.",
"install_devices_router_desc": "Această configurare acoperă automat toate dispozitivele conectate la routerul de acasă, nu este nevoie să le configurați manual.",
"install_devices_address": "Serverul DNS AdGuard Home ascultă pe următoarele adrese",
"install_devices_router_list_1": "Deschideți preferințele routerului dvs. De obicei, îl puteți accesa din browser printr-o adresă URL cum ar fi http://192.168.0.1/ sau http://192.168.1.1/. Vi se poate cere să introduceți o parolă. Dacă nu v-o amintiți, adesea puteți reseta parola apăsând un buton de pe routerul propriu-zis, dar fiți conștienți, că prin acest procedeu puteți pierde întreaga configurație a routerului. \nDacă routerul dvs. necesită o aplicație pentru configurare, instalați aplicația pe telefon sau pe PC și utilizați-o pentru a accesa setările routerului.",
"install_devices_router_list_2": "Găsiți setările DHCP/DNS. Căutați literele DNS lângă un câmp care să permită două sau trei seturi de numere, fiecare împărțit în patru grupuri de una până la trei cifre.",
"install_devices_router_list_3": "Introduceți adresele serverului dvs. AdGuard Home aici.",
"install_devices_router_list_4": "Unele tipuri de routere, nu permit configurarea unui server DNS personalizat. În acest caz, configurarea AdGuard Home ca un <0>server DHCP</0>vă poate ajuta. Dacă nu, ar trebui verificat manualul routerului dvs. specific, ca să aflați cum se pot personaliza serverele DNS.",
"install_devices_windows_list_1": "Deschideți panoul de control prin meniul Start sau căutare Windows.",
"install_devices_windows_list_2": "Accesați categoria \"Rețea și Internet\", apoi la \"Centrul de Rețea și Partajare\".",
"install_devices_windows_list_3": "În partea stângă a ecranului găsiți \"Schimbare setări adaptor\" și clicați pe el.",
@@ -299,7 +336,7 @@
"install_devices_android_list_2": "Tapați Wi-Fi din meniu. Ecranul cu toate rețelele disponibile va fi afișat (este imposibil să setați DNS personalizat pentru conexiunea mobilă).",
"install_devices_android_list_3": "Apăsați lung pe rețeaua la care sunteți conectat și tapați Modificare Rețea.",
"install_devices_android_list_4": "Pe unele aparate, poate fi necesar să bifați caseta Advanced pentru a vedea setările adiționale. Pentru a ajusta setările DNS Android, va trebui să comutați setările IP de la DHCP la Static.",
"install_devices_android_list_5": "Schimbați valorile DNS 1 și DNS 2 la cele ale serverului dvs. AdGuard Home.",
"install_devices_android_list_5": "Schimbați valorile DNS 1 și DNS 2 la adresele serverului dvs. AdGuard Home.",
"install_devices_ios_list_1": "Din ecranul de start, tapați Setări.",
"install_devices_ios_list_2": "Alegeți Wi-Fi în meniul din stânga (este imposibil să configurați DNS pentru rețelele mobile).",
"install_devices_ios_list_3": "Tapați numele rețelei active curente.",
@@ -310,6 +347,7 @@
"install_saved": "Salvat cu succes",
"encryption_title": "Criptare",
"encryption_desc": "Suport de Criptare (HTTPS/TLS) pentru DNS și interfața web administrator",
"encryption_config_saved": "Configurația de criptare salvată",
"encryption_server": "Nume de server",
"encryption_server_enter": "Introduceți numele domeniului",
"encryption_server_desc": "Pentru a utiliza HTTPS, trebuie să introduceți numele serverului care se potrivește cu certificatul SSL sau certificatul wildcard al dvs. În cazul în care câmpul nu este setat, va accepta conexiuni TLS pentru orice domeniu.",
@@ -340,10 +378,13 @@
"encryption_reset": "Sunteți sigur că doriți să resetați setările de criptare?",
"topline_expiring_certificate": "Certificatul dvs. SSL este pe cale să expire. Actualizați <0>Setările de criptare</0>.",
"topline_expired_certificate": "Certificatul dvs. SSL a expirat. Actualizați <0>Setările de criptare</0>.",
"form_error_port_range": "Introduceți valoarea portului între 80-65535",
"form_error_port_unsafe": "Acesta este un port nesigur",
"form_error_equal": "Nu trebuie să fie egale",
"form_error_password": "Parolele nu corespund",
"reset_settings": "Resetare setări",
"update_announcement": "AdGuard Home {{version}} este disponibil! <0>Clicați aici</0> pentru mai multe informații.",
"setup_guide": "Ghid de instalare",
"dns_addresses": "Adrese DNS",
"dns_start": "Serverul DNS demarează",
"dns_status_error": "Eroare la verificare statut server DNS",
@@ -365,8 +406,9 @@
"client_edit": "Editare client",
"client_identifier": "Identificator",
"ip_address": "Adresa IP",
"client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC sau un ID special al clientului (poate fi folosit pentru DoT/DoH/DoQ). <0>Aici</0> puteți afla mai multe despre cum să identificați clienții.",
"client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC sau un ID client special (poate fi folosit pentru DoT/DoH/DoQ). <0>Aici</0> puteți afla mai multe despre cum să identificați clienții.",
"form_enter_ip": "Introduceți IP",
"form_enter_subnet_ip": "Introduceți o adresă IP în subrețeaua „{{cidr}}”",
"form_enter_mac": "Introduceți MAC",
"form_enter_id": "Introduceți identificator",
"form_add_id": "Adăugați identificator",
@@ -384,10 +426,11 @@
"access_title": "Setări de acces",
"access_desc": "Aici puteți configura regulile de acces pentru serverul DNS AdGuard Home.",
"access_allowed_title": "Clienți autorizați",
"access_allowed_desc": "O listă de adrese CIDR sau IP. Dacă este configurat, AdGuard Home va accepta doar cereri de la aceste adrese IP.",
"access_allowed_desc": "O listă de adrese CIDR sau IP client. Dacă este configurat, AdGuard Home va accepta cereri numai de la acești clienți.",
"access_disallowed_title": "Clienți neautorizați",
"access_disallowed_desc": "O listă de adrese CIDR sau IP. Dacă este configurat, AdGuard Home va elimina cererile de la aceste adrese IP.",
"access_disallowed_desc": "O listă de adrese CIDR sau IP. Dacă este configurat, AdGuard Home va elimina cereri de la acești clienți. Dacă sunt configurați clienți permiși, acest câmp este ignorat.",
"access_blocked_title": "Domenii blocate",
"access_blocked_desc": "A nu se confunda cu filtrele. AdGuard Home respinge cererile DNS pentru aceste domenii, iar aceste cereri nici măcar nu apar în jurnalul de solicitări. Puteți specifica nume exacte de domenii, metacaractere sau reguli de filtrare URL, cum ar fi \"example.org\", \"*.exemple.org\" sau \"||example.org^\" în mod corespunzător.",
"access_settings_saved": "Setările de acces au fost salvate cu succes",
"updates_checked": "Actualizările au fost verificate cu succes",
"updates_version_equal": "AdGuard Home este la zi",
@@ -441,10 +484,12 @@
"encryption_key_source_content": "Lipiți conținutul cheii private",
"stats_params": "Configurația statisticilor",
"config_successfully_saved": "Configurarea a fost salvată cu succes",
"interval_6_hour": "6 ore",
"interval_24_hour": "24 ore",
"interval_days": "{{count}} zi",
"interval_days_plural": "{{count}} zile",
"domain": "Domeniu",
"punycode": "Punycode",
"answer": "Răspuns",
"filter_added_successfully": "Filtrul a fost adăugat cu succes",
"filter_removed_successfully": "Lista a fost eliminată cu succes",
@@ -487,9 +532,10 @@
"rewrite_domain_name": "Nume de domeniu: adăugați o înregistrare CNAME",
"rewrite_A": "<0>A</0>: valoare specială, păstrați <0>A</0> înregistrări din amonte",
"rewrite_AAAA": "<0>AAAA</0>: valoare specială, păstrați <0>AAAA</0> înregistrări din amonte",
"disable_ipv6": "Dezactivați IPv6",
"disable_ipv6_desc": "Dacă această opțiune este activată, toate interogările DNS pentru adrese IPv6 (tip AAAA) vor fi anulate.",
"disable_ipv6": "Dezactivați rezolvarea adreselor IPv6",
"disable_ipv6_desc": "Anulați toate interogările DNS pentru adresele IPv6 (tip AAAA).",
"fastest_addr": "Cea mai rapidă adresă IP",
"fastest_addr_desc": "Interoghează toate serverele DNS și întoarce adresa IP cea mai rapidă din răspunsuri. Acest lucru încetinește interogările DNS, deoarece AdGuard Home trebuie să aștepte răspunsuri de la toate serverele DNS, dar îmbunătățește conectivitatea generală.",
"autofix_warning_text": "Dacă clicați pe \"Fix\", AdGuardHome va configura sistemul dvs. pentru a utiliza serverul DNS AdGuardHome.",
"autofix_warning_list": "Va efectua aceste sarcini: <0>Dezactivare sistem DNSStubListener</0> <0>Setare adresă server DNS la 127.0.0.1</0> <0>Înlocuire link simbolic țintă /etc/resolv.conf cu /run/systemd/resolve/resolv.conf</0> <0>Oprire DNSStubListener (reîncărcare servici rezolvat prin sistem)</0>",
"autofix_warning_result": "Ca urmare, toate cererile DNS ale sistemul dvs. vor fi procesate în mod implicit de AdGuardHome.",
@@ -519,11 +565,12 @@
"set_static_ip": "Setați o adresă IP statică",
"install_static_ok": "Vești bune! Adresa IP statică este deja configurată",
"install_static_error": "AdGuard Home nu o poate configura automat pentru această interfață de rețea. Vă rugăm să căutați instrucțiuni despre cum să faceți acest lucru manual.",
"install_static_configure": "AdGuard Home a detectat că se folosește adresa IP dinamică <0>{{ip}}</0>. Doriți ca aceasta să fie setată ca adresă statică?",
"confirm_static_ip": "AdGuard Home va configura {{ip}} ca adresa dvs. IP statică. Doriți să continuați?",
"list_updated": "{{count}} listă actualizată",
"list_updated_plural": "{{count}} liste actualizate",
"dnssec_enable": "Activați DNSSEC",
"dnssec_enable_desc": "Setați steagul DNSSEC pe interogările DNS de ieșire și verificați rezultatul (este necesar un resolver DNSSEC activat)",
"dnssec_enable_desc": "Activați semnalul DNSSEC în interogările DNS de ieșire și verificați rezultatul (este necesar un resolver compatibil DNSSEC).",
"validated_with_dnssec": "Validat cu DNSSEC",
"all_queries": "Toate interogările",
"show_blocked_responses": "Blocat",
@@ -553,9 +600,10 @@
"filter_category_regional": "Regional",
"filter_category_other": "Altele",
"filter_category_general_desc": "Liste care blochează urmărirea și publicitatea pe majoritatea aparatelor",
"filter_category_security_desc": "Liste specializate în blocarea domeniilor malware, phishing sau înșelătorie",
"filter_category_security_desc": "Listele concepute special pentru a bloca domenii rău intenționate, phishing și înșelătorie",
"filter_category_regional_desc": "Liste focalizate pe reclame regionale și servere de urmărire",
"filter_category_other_desc": "Alte liste de blocări",
"setup_config_to_enable_dhcp_server": "Setați configurația pentru a activa serverul DHCP",
"original_response": "Răspuns original",
"click_to_view_queries": "Clicați pentru a vizualiza interogări",
"port_53_faq_link": "Portul 53 este adesea ocupat de serviciile \"DNSStubListener\" sau \"systemd-resolved\". Vă rugăm să citiți <0>această instrucțiune</0> despre cum să rezolvați aceasta.",

View File

@@ -5,7 +5,7 @@
"upstream_parallel": "Использовать параллельные запросы ко всем серверам одновременно для ускорения обработки запроса.",
"parallel_requests": "Параллельные запросы",
"load_balancing": "Распределение нагрузки\n",
"load_balancing_desc": "Запрашивайте по одному серверу за раз. AdGuard Home использует случайный алгоритм для выбора сервера, так что самый быстрый сервер используется чаще.",
"load_balancing_desc": "Запрашивать по одному серверу за раз. AdGuard Home использует алгоритм взвешенного случайного выбора сервера, так что самый быстрый сервер используется чаще.",
"bootstrap_dns": "Bootstrap DNS-серверы",
"bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.",
"local_ptr_title": "Приватные серверы для обратного DNS",
@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Новая статическая аренда",
"dhcp_static_leases_not_found": "Не найдено статических аренд DHCP",
"dhcp_add_static_lease": "Добавить статическую аренду",
"dhcp_reset_leases": "Сбросить все аренды",
"dhcp_reset_leases_confirm": "Вы уверены, что хотите удалить все аренды?",
"dhcp_reset_leases_success": "Аренды DHCP успешно удалены",
"dhcp_reset": "Вы уверены, что хотите сбросить настройки DHCP?",
"country": "Страна",
"city": "Город",
@@ -423,9 +426,9 @@
"access_title": "Настройки доступа",
"access_desc": "Здесь вы можете настроить правила доступа к DNS-серверу AdGuard Home.",
"access_allowed_title": "Разрешённые клиенты",
"access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.",
"access_allowed_desc": "Список CIDR, IP-адресов или ID клиентов. Если он настроен, AdGuard Home будет принимать запросы только от этих клиентов.",
"access_disallowed_title": "Запрещённые клиенты",
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
"access_disallowed_desc": "Список CIDR, IP-адресов или ID клиентов. Если он настроен, AdGuard Home будет игнорировать запросы от этих клиентов. Если настроены разрешённые клиенты, это поле игнорируется.",
"access_blocked_title": "Неразрешённые домены",
"access_blocked_desc": "Не путать с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами. Здесь вы можете уточнить точные имена доменов, шаблоны, правила URL-фильтрации, например, «example.org», «*.example.org» или «||example.org».",
"access_settings_saved": "Настройки доступа успешно сохранены",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Вставить содержимое закрытого ключа",
"stats_params": "Конфигурация статистики",
"config_successfully_saved": "Конфигурация успешно сохранена",
"interval_6_hour": "6 часов",
"interval_24_hour": "24 часа",
"interval_days": "{{count}} день",
"interval_days_plural": "{{count}} дней",
"domain": "Домен",
"punycode": "Punycode",
"answer": "Ответ",
"filter_added_successfully": "Список успешно добавлен",
"filter_removed_successfully": "Список успешно удалён",

View File

@@ -68,6 +68,9 @@
"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_leases": "Resetovať všetky prenájmy",
"dhcp_reset_leases_confirm": "Naozaj chcete resetovať všetky prenájmy?",
"dhcp_reset_leases_success": "DHCP prenájmy boli úspešne resetované",
"dhcp_reset": "Naozaj chcete vymazať DHCP konfiguráciu?",
"country": "Krajina",
"city": "Mesto",
@@ -481,10 +484,12 @@
"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_6_hour": "6 hodín",
"interval_24_hour": "24 hodín",
"interval_days": "{{count}} deň",
"interval_days_plural": "{{count}} dní",
"domain": "Doména",
"punycode": "Punycode",
"answer": "Odpoveď",
"filter_added_successfully": "Filter bol úspešne pridaný",
"filter_removed_successfully": "Zoznam bol úspešne odstránený",

View File

@@ -14,9 +14,9 @@
"local_ptr_no_default_resolver": "AdGuard Home, bu sistem için uygun özel ters DNS çözümleyicilerini belirleyemedi.",
"local_ptr_placeholder": "Her satıra bir sunucu adresi girin",
"resolve_clients_title": "İstemcilerin IP adreslerinin ters çözümlenmesini etkinleştir",
"resolve_clients_desc": "Etkinleştirilirse, AdGuard Home ilgili çözümleyicilere (yerel istemciler için özel DNS sunucuları, genel IP adreslerine sahip istemciler için üst sunucusu) PTR sorguları göndererek istemcilerin IP adreslerini ana bilgisayar adlarına ters olarak çözümlemeye çalışır.",
"resolve_clients_desc": "Karşılık gelen çözümleyicilere (yerel istemciler için özel DNS sunucuları, genel IP adresleri olan istemciler için üst sunucuları) PTR sorguları göndererek istemcilerin IP adreslerini ana bilgisayar adlarına tersine çözün.",
"use_private_ptr_resolvers_title": "Özel DNS çözümleyicileri kullan",
"use_private_ptr_resolvers_desc": "Bu üst kaynak sunucularını kullanarak yerel olarak sunulan adresler için ters DNS aramaları gerçekleştirin. Devre dışı bırakılırsa, AdGuard Home, DHCP, /etc/hosts, vb. tarafından bilinen istemciler dışında bu tür tüm PTR isteklerine NXDOMAIN ile yanıt verir.",
"use_private_ptr_resolvers_desc": "Bu üst kaynak sunucularını kullanarak yerel olarak sunulan adresler için ters DNS aramaları yapın. Devre dışı bırakılırsa, AdGuard Home, DHCP, /etc/hosts, vb. tarafından bilinen istemciler dışında bu tür tüm PTR isteklerine NXDOMAIN ile yanıt verir.",
"check_dhcp_servers": "DHCP sunucularını denetle",
"save_config": "Yapılandırmayı kaydet",
"enabled_dhcp": "DHCP sunucusu etkinleştirildi",
@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "Yeni sabit kiralama",
"dhcp_static_leases_not_found": "Sabit DHCP kiralaması bulunamadı",
"dhcp_add_static_lease": "Sabit kiralama ekle",
"dhcp_reset_leases": "Tüm kiralamaları sıfırla",
"dhcp_reset_leases_confirm": "Tüm kiralamaları sıfırlamak istediğinizden emin misiniz?",
"dhcp_reset_leases_success": "DHCP kiralamaları başarıyla sıfırlandı",
"dhcp_reset": "DHCP yapılandırmasını sıfırlamak istediğinizden emin misiniz?",
"country": "Ülke",
"city": "Şehir",
@@ -99,7 +102,7 @@
"enable_protection": "Korumayı etkinleştir",
"enabled_protection": "Koruma etkileştirildi",
"disable_protection": "Korumayı durdur",
"disabled_protection": "Koruma devre dışı",
"disabled_protection": "Koruma durduruldu",
"refresh_statics": "İstatistikleri yenile",
"dns_query": "DNS Sorguları",
"blocked_by": "<0>Filtreler tarafından engellendi</0>",
@@ -128,11 +131,11 @@
"block_domain_use_filters_and_hosts": "Filtre ve ana bilgisayar listelerini kullanarak alan adlarını engelle",
"filters_block_toggle_hint": "<a>Filtreler</a> sayfasından engelleme kurallarını ayarlayabilirsiniz.",
"use_adguard_browsing_sec": "AdGuard gezinti koruması web hizmetini kullan",
"use_adguard_browsing_sec_hint": "AdGuard Home, alan adının gezinti koruması web hizmetinde kara listede olup olmadığını kontrol edecek. Kontrol işlemi gizlilik dostu API kullanılarak yapılacak: yalnızca alan adının kısa bir ön eki SHA256 ile şifrelenip sunucuya gönderilecek.",
"use_adguard_browsing_sec_hint": "AdGuard Home, alan adının gezinti koruması web hizmeti tarafından engellenip engellenmediğini kontrol eder. Kontrolü gerçekleştirmek için gizlilik dostu arama API'sini kullanır: sunucuya yalnızca SHA256 karma alan adının kısa bir ön eki gönderilir.",
"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ı kullan",
"enforce_save_search_hint": "AdGuard Home şu arama motorlarında güvenli aramayı zorunlu kılabilir: Google, YouTube, Bing, DuckDuckGo, Yandex ve Pixabay.",
"enforce_save_search_hint": "AdGuard Home, şu arama motorlarında güvenli aramayı zorunlu kılar: Google, YouTube, Bing, DuckDuckGo, Yandex ve Pixabay.",
"no_servers_specified": "Sunucu adresi girilmedi",
"general_settings": "Genel ayarlar",
"dns_settings": "DNS ayarları",
@@ -167,7 +170,7 @@
"edit_table_action": "Düzenle",
"delete_table_action": "Sil",
"elapsed": "Geçen zaman",
"filters_and_hosts_hint": "AdGuard Home temel reklam engelleme kuralları ve ana bilgisayar dosyalarının sözdizim kurallarını anlamaktadır.",
"filters_and_hosts_hint": "AdGuard Home, temel reklam engelleme kurallarını anlar ve dosya söz dizimini barındırır.",
"no_blocklist_added": "Engelleme listesi eklenmedi",
"no_whitelist_added": "İzin verilen listesi eklenmedi",
"add_blocklist": "Engelleme listesi ekle",
@@ -187,7 +190,7 @@
"form_error_url_format": "Geçersiz URL biçimi",
"form_error_url_or_path_format": "Geçersiz URL ya da listenin tam yolu",
"custom_filter_rules": "Özel filtreleme kuralları",
"custom_filter_rules_hint": "Her satıra bir kural girin. Reklama engelleme kuralı veya ana bilgisayar dosyası sözdizimi kullanabilirsiniz.",
"custom_filter_rules_hint": "Her satıra bir kural girin. Reklama engelleme kuralı veya ana bilgisayar dosyası söz dizimi kullanabilirsiniz.",
"examples_title": "Örnekler",
"example_meaning_filter_block": "example.org alan adına ve tüm alt alan adlarına olan erişimi engeller",
"example_meaning_filter_whitelist": "example.org alan adına ve tüm alt alan adlarına olan erişim engelini kaldırır",
@@ -211,7 +214,7 @@
"disallow_this_client": "Bu istemciye izin verme",
"allow_this_client": "Bu istemciye izin ver",
"block_for_this_client_only": "Yalnızca bu istemci için engelle",
"unblock_for_this_client_only": "Yalnızca bu müşteri için engellemeyi kaldır",
"unblock_for_this_client_only": "Yalnızca bu istemci için engellemeyi kaldır",
"time_table_header": "Saat",
"date": "Tarih",
"domain_name_table_header": "Alan adı",
@@ -284,7 +287,7 @@
"upstream_dns_client_desc": "Bu alanı boş tutarsanız, AdGuard Home, <0>DNS ayarlarında</0> yapılandırılmış sunucuları kullanır.",
"tracker_source": "İzleyici kaynağı",
"source_label": "Kaynak",
"found_in_known_domain_db": "Bilinen alan adları veritabanı içinde bulundu.",
"found_in_known_domain_db": "Bilinen alan adları veri tabanı içinde bulundu.",
"category_label": "Kategori",
"rule_label": "Kural",
"list_label": "Liste",
@@ -306,14 +309,14 @@
"install_auth_password": "Parola",
"install_auth_confirm": "Parolayı onayla",
"install_auth_username_enter": "Kullanıcı adı girin",
"install_auth_password_enter": "Şifre girin",
"install_auth_password_enter": "Parola girin",
"install_step": "Adım",
"install_devices_title": "Cihazlarınızı ayarlayın",
"install_devices_desc": "AdGuard Home'un çalışması için cihazlarınızı onu kullanacak şekilde ayarlamalısınız.",
"install_submit_title": "Tebrikler!",
"install_submit_desc": "Kurulum prosedürü tamamlandı ve artık AdGuard Home'u kullanmaya hazırsınız.",
"install_devices_router": "Yönlendirici",
"install_devices_router_desc": "Bu kurulum evdeki yönlendiricinize bağlı tüm cihazlarınızı otomatik olarak kapsar ve her birini elle ayarlamanız gerekmez.",
"install_devices_router_desc": "Bu kurulum, ev yönlendiricinize bağlı tüm cihazları otomatik olarak kapsar ve her birini elle yapılandırmanıza gerek yoktur.",
"install_devices_address": "AdGuard Home DNS sunucusu şu adresi dinleyecektir",
"install_devices_router_list_1": "Yönlendiricinizin ayarlarına girin. Genellikle tarayıcınızdan http://192.168.0.1/ veya http://192.168.1.1/ gibi bir URL aracılığıyla erişebilirsiniz. Bir parola girmeniz istenebilir. Hatırlamıyorsanız, genellikle yönlendiricinin üzerindeki bir düğmeye basarak parolayı sıfırlayabilirsiniz, ancak bu yöntemin seçilmesi durumunda muhtemelen tüm yönlendirici yapılandırmasını kaybedeceğinizi unutmayın. Yönlendiricinizin kurulması için bir uygulama gerekiyorsa, lütfen uygulamayı telefonunuza veya PC'nize yükleyin ve yönlendiricinin ayarlarına erişmek için kullanın.",
"install_devices_router_list_2": "DHCP/DNS ayarlarını bulun. DNS satırlarını arayın, genelde iki veya üç tanedir, üç rakam girilebilen dört ayrı grup içeren satırdır.",
@@ -338,7 +341,7 @@
"install_devices_ios_list_2": "Sol menüdeki Wi-Fi bölümüne girin (mobil bağlantı için isteğe bağlı DNS sunucusu ayarlanamaz).",
"install_devices_ios_list_3": "Bağlı olduğunuz ağın ismine dokunun.",
"install_devices_ios_list_4": "DNS alanına AdGuard Home sunucunuzun adreslerini girin.",
"get_started": "Başlarken",
"get_started": "Başlayın",
"next": "Sonraki",
"open_dashboard": "Panoyu Aç",
"install_saved": "Başarıyla kaydedildi",
@@ -423,11 +426,11 @@
"access_title": "Erişim ayarları",
"access_desc": "Burada, AdGuard Home DNS sunucusu için erişim kurallarını yapılandırabilirsiniz.",
"access_allowed_title": "İzin verilen istemciler",
"access_allowed_desc": "CIDR veya IP adreslerinin listesi. Yapılandırılırsa, AdGuard Home yalnızca bu IP adreslerinden gelen istekleri kabul edecektir.",
"access_allowed_desc": "CIDR'lerin, IP adreslerinin veya istemci kimliklerinin listesi. Yapılandırılırsa, AdGuard Home yalnızca bu istemcilerden gelen istekleri kabul eder.",
"access_disallowed_title": "İzin verilmeyen istemciler",
"access_disallowed_desc": "CIDR veya IP adreslerinin listesi. Yapılandırılırsa, AdGuard Home yalnızca bu IP adreslerinden gelen istekleri cevapsız bırakacaktır.",
"access_disallowed_desc": "CIDR'lerin, IP adreslerinin veya istemci kimliklerinin listesi. Yapılandırılırsa, AdGuard Home bu istemcilerden gelen istekleri keser. İzin verilen istemciler yapılandırılırsa, bu alan yok sayılır.",
"access_blocked_title": "Engellenmiş alan adları",
"access_blocked_desc": "Bunu filtrelerle karıştırmayın. AdGuard Home, sorguların sorularına bu alan adlarıyla DNS sorgularını bırakacaktır. Burada tam alan adlarını, joker karakterleri ve URL filtre kurallarını belirtebilirsiniz, örn. \"example.org\", \"*.example.org\" veya \"|| example.org^\".",
"access_blocked_desc": "Filtrelerle karıştırılmamalıdır. AdGuard Home, bu alan adlarıyla eşleşen DNS sorgularını keser ve bu sorgular sorgu günlüğünde bile görünmez. Tam alan adlarını, joker karakterleri veya URL filtre kurallarını belirtebilirsiniz, ör. \"example.org\", \"*.example.org\" veya \"||example.org^\" uygun şekilde.",
"access_settings_saved": "Erişim ayarları başarıyla kaydedildi!",
"updates_checked": "Güncelleme kontrolü başarılı",
"updates_version_equal": "AdGuard Home yazılımı günceldir",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "Özel anahtar içeriklerini yapıştırın",
"stats_params": "İstatistik yapılandırması",
"config_successfully_saved": "Yapılandırma başarıyla kaydedildi",
"interval_6_hour": "6 saat",
"interval_24_hour": "24 saat",
"interval_days": "{{count}} gün",
"interval_days_plural": "{{count}} gün",
"domain": "Alan adı",
"punycode": "Punycode",
"answer": "Cevap",
"filter_added_successfully": "Filtre başarıyla eklendi",
"filter_removed_successfully": "Filtre başarıyla kaldırıldı",
@@ -544,7 +549,7 @@
"filtered_custom_rules": "Özel filtreleme kurallarına göre filtrelendi",
"choose_from_list": "Listeden seç",
"add_custom_list": "Özel bir liste ekle",
"host_whitelisted": "Ana bilgisayar beyaz listeye eklendi",
"host_whitelisted": "Ana bilgisayara izin verildi",
"check_ip": "IP adresleri: {{ip}}",
"check_cname": "CNAME: {{cname}}",
"check_reason": "Sebep: {{reason}}",
@@ -565,11 +570,11 @@
"list_updated": "{{count}} liste güncellendi",
"list_updated_plural": "{{count}} liste güncellendi",
"dnssec_enable": "DNSSEC'i etkinleştir",
"dnssec_enable_desc": "Gelen DNS sorgularında DNSSEC bayrağını ayarlayın ve sonucu kontrol edin (DNSSEC etkin çözümleyici gereklidir).",
"dnssec_enable_desc": "DNSSEC'i giden DNS sorguları için etkinleştir ve sonucu kontrol et (DNSSEC-etkin sorgulama gerekli).",
"validated_with_dnssec": "DNSSEC ile doğrulandı",
"all_queries": "Tüm sorgular",
"show_blocked_responses": "Engellendi",
"show_whitelisted_responses": "Beyaz listeye eklendi",
"show_whitelisted_responses": "İzin verildi",
"show_processed_responses": "İşlendi",
"blocked_safebrowsing": "Güvenli gezinti tarafından engellendi",
"blocked_adult_websites": "Yetişkin içerikli site engellendi",

View File

@@ -68,6 +68,9 @@
"dhcp_new_static_lease": "新建静态租约",
"dhcp_static_leases_not_found": "未找到 DHCP 静态租约",
"dhcp_add_static_lease": "添加静态租约",
"dhcp_reset_leases": "重置所有租约",
"dhcp_reset_leases_confirm": "您确定您想重置所有租约吗?",
"dhcp_reset_leases_success": "成功重置了 DHCP 租约",
"dhcp_reset": "您确定要重置 DHCP 设定吗?",
"country": "国家",
"city": "城市",
@@ -423,9 +426,9 @@
"access_title": "访问设置",
"access_desc": "您可在此处配置 AdGuard Home DNS 服务器的访问规则。",
"access_allowed_title": "允许的客户端",
"access_allowed_desc": "CIDRIP 地址列表。如配置,则 AdGuard Home 仅接受自这些 IP 地址的请求。",
"access_allowed_desc": "CIDRIP 地址或客户端 ID 的列表。如配置,则 AdGuard Home 仅接受自这些客户端的请求。",
"access_disallowed_title": "不允许的客户端",
"access_disallowed_desc": "CIDR 或 IP 地址列表。如配置,则 AdGuard Home 会放弃源自这些 IP 地址的请求。",
"access_disallowed_desc": "CIDR、IP 地址或客户端 ID 的列表。如果已配置,则 AdGuard Home 将丢弃来自这些 IP 地址的请求。如果允许的客户端已配置,此字段将会被忽略。",
"access_blocked_title": "不允许的域名",
"access_blocked_desc": "不要将此功能与过滤器混淆。AdGuard Home 将排除匹配这些网域的 DNS 查询并且这些查询将不会在查询日志中显示。在此可以明确指定域名、通配符wildcard和网址过滤的规则例如 \"example.org\"、\"*.example.org\" 或 \"||example.org^\"。",
"access_settings_saved": "访问设置保存成功",
@@ -485,6 +488,7 @@
"interval_days": "{{count}} 天",
"interval_days_plural": "{{count}} 天",
"domain": "域名",
"punycode": "Punycode",
"answer": "应答",
"filter_added_successfully": "已成功添加过滤器",
"filter_removed_successfully": "已成功删除该列表",

View File

@@ -9,10 +9,14 @@
"bootstrap_dns": "引導Boostrap DNS 伺服器",
"bootstrap_dns_desc": "Bootstrap DNS 伺服器用於解析您所設定的上游 DoH/DoT 解析器的 IP 地址",
"local_ptr_title": "私人 DNS 伺服器",
"local_ptr_desc": "AdGuard Home 用於區域 PTR 查詢的 DNS 伺服器。伺服器將用於解析具有私人 IP 位址的用戶端的主機名稱,比如 \"192.168.12.34\",使用 rDNS。如果沒有設定AdGuard Home 將自動使用您的預設 DNS 解析。",
"local_ptr_desc": "AdGuard Home 用於區域 PTR 查詢的 DNS 伺服器。這些伺服器將用於解析具有私人 IP 位址的用戶端的主機名稱,例如 \"192.168.12.34\",使用 rDNS。如果沒有設定AdGuard Home 將自動使用您的系統預設 DNS 解析。",
"local_ptr_default_resolver": "AdGuard Home 預設使用以下作為 DNS 反解器:{{ip}}",
"local_ptr_no_default_resolver": "AdGuard Home 無法為此系統確定適合私有反解析器。",
"local_ptr_placeholder": "每行輸入一個伺服器位址",
"resolve_clients_title": "啟用用戶端的 IP 位址的反向解析",
"resolve_clients_desc": "如果啟用AdGuard Home 將嘗試透過傳送 PTR 查詢到對應的解析器 (本機用戶端私人 DNS 伺服器,公 IP 用戶端的上遊伺服器) ,將 IP 位址反向解析成其用戶端的主機名稱。",
"resolve_clients_desc": "透過相應的伺服器傳送 PTR 查詢(本機用戶端使用私人 DNS 伺服器,公 IP 使用上游伺服器),將客戶端的 IP 反解為主機名稱。",
"use_private_ptr_resolvers_title": "使用私人 DNS 反解器",
"use_private_ptr_resolvers_desc": "使用這些上游為本機提供反解 DNS 查詢。如果禁用 AdGuard Home 將會對相關 PTR 請求回應 NXDOMAIN但 DHCP、/etc/hosts 等已知的用戶端除外。",
"check_dhcp_servers": "檢查 DHCP 伺服器",
"save_config": "儲存設定",
"enabled_dhcp": "DHCP 伺服器已啟動",
@@ -64,6 +68,9 @@
"dhcp_new_static_lease": "新增靜態租用",
"dhcp_static_leases_not_found": "找不到 DHCP 靜態租用",
"dhcp_add_static_lease": "新增靜態租用",
"dhcp_reset_leases": "重置所有 DHCP 租約",
"dhcp_reset_leases_confirm": "您確定要重設所有 DHCP 租約嗎?",
"dhcp_reset_leases_success": "重置 DHCP 租約成功",
"dhcp_reset": "您確定要重設 DHCP 設定嗎?",
"country": "國家",
"city": "城市",
@@ -128,7 +135,7 @@
"use_adguard_parental": "使用 AdGuard 家長監護功能",
"use_adguard_parental_hint": "AdGuard Home 將比對查詢網域是否含有成人內容。它使用與 AdGuard 瀏覽安全一樣的尊重個人隱私的 API 來進行檢查。",
"enforce_safe_search": "強制使用安全搜尋",
"enforce_save_search_hint": "AdGuard Home 可在下列搜尋引擎使用強制安全搜尋Google、YouTube、Bing、DuckDuckGo Yandex。",
"enforce_save_search_hint": "AdGuard Home 可在下列搜尋引擎使用強制安全搜尋Google、YouTube、Bing、DuckDuckGo, Yandex 和 Pixabay。",
"no_servers_specified": "沒有指定的伺服器",
"general_settings": "一般設定",
"dns_settings": "DNS 設定",
@@ -268,7 +275,7 @@
"form_enter_rate_limit": "輸入速率限制",
"rate_limit": "速率限制",
"edns_enable": "啟用 EDNS Client Subnet",
"edns_cs_desc": "開啟後 AdGuard Home 將會傳送用戶端的子網路給 DNS 伺服器。",
"edns_cs_desc": "傳送用戶端的子網路給 DNS 伺服器。",
"rate_limit_desc": "限制單一裝置每秒發出的查詢次數(設定為 0 即表示無限制)",
"blocking_ipv4_desc": "回覆指定 IPv4 位址給被封鎖的網域的 A 紀錄查詢",
"blocking_ipv6_desc": "回覆指定 IPv6 位址給被封鎖的網域的 AAAA 紀錄查詢",
@@ -307,7 +314,7 @@
"install_devices_title": "設定您的裝置",
"install_devices_desc": "要開始使用 AdGuard Home您需要設定好裝置才能使用。",
"install_submit_title": "恭喜!",
"install_submit_desc": "安裝步驟已完成,現在已經可以開始使用 AdGuard Home",
"install_submit_desc": "安裝步驟已完成,現在已經可以開始使用 AdGuard Home",
"install_devices_router": "路由器",
"install_devices_router_desc": "使用此設定後,所有連接家中路由器的裝置都會自動套用,無須在每台裝置上個別設定。",
"install_devices_address": "AdGuard Home DNS 伺服器正在監聽以下位址",
@@ -399,7 +406,7 @@
"client_edit": "編輯用戶端",
"client_identifier": "識別碼",
"ip_address": "IP 位址",
"client_identifier_desc": "可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置。注意:必須使用 AdGuard Home 內建 <0>DHCP 伺服器</0> 才能偵測 MAC 地址。",
"client_identifier_desc": "可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置,或使用個別客戶端 ID (可用於 DoT/DoH/DoQ。<0>點擊這裡</0>進一步了解關於辨識使用者裝置。",
"form_enter_ip": "輸入 IP",
"form_enter_subnet_ip": "在子網路 \"{{cidr}}\" 中輸入一個 IP 位址",
"form_enter_mac": "輸入 MAC 地址",
@@ -523,7 +530,7 @@
"rewrite_domain_name": "網域名稱:新增一筆 CNAME 紀錄",
"rewrite_A": "<0>A</0>: 特殊值,將上游查詢結果覆寫 <0>A</0> 紀錄",
"rewrite_AAAA": "<0>AAAA</0>: 特殊值,將上游查詢結果覆寫 <0>AAAA</0> 紀錄",
"disable_ipv6": "停 IPv6",
"disable_ipv6": "停止解析 IPv6 位址",
"disable_ipv6_desc": "開啟此功能後,將捨棄所有對 IPv6 位址AAAA的查詢。",
"fastest_addr": "Fastest IP 位址",
"fastest_addr_desc": "從所有 DNS 伺服器查詢中回應最快的 IP 位址。但這操作會等待所有 DNS 查詢結果後才能回應,導致速度有所降低,不過同時卻也改善了整體連線品質。",

View File

@@ -67,7 +67,10 @@
"dhcp_lease_deleted": "靜態租約 \"{{key}}\" 被成功地刪除",
"dhcp_new_static_lease": "新的靜態租約",
"dhcp_static_leases_not_found": "無已發現之動態主機設定協定DHCP靜態租約",
"dhcp_add_static_lease": "增靜態租約",
"dhcp_add_static_lease": "增靜態租約",
"dhcp_reset_leases": "重置所有的租約",
"dhcp_reset_leases_confirm": "您確定您想要重置所有的租約嗎?",
"dhcp_reset_leases_success": "動態主機設定協定DHCP租約被成功地重置",
"dhcp_reset": "您確定您想要重置動態主機設定協定DHCP配置嗎",
"country": "國家",
"city": "城市",
@@ -170,8 +173,8 @@
"filters_and_hosts_hint": "AdGuard Home 懂得基本的廣告封鎖規則和主機檔案語法。",
"no_blocklist_added": "無已加入的封鎖清單",
"no_whitelist_added": "無已加入的允許清單",
"add_blocklist": "增封鎖清單",
"add_allowlist": "增允許清單",
"add_blocklist": "增封鎖清單",
"add_allowlist": "增允許清單",
"cancel_btn": "取消",
"enter_name_hint": "輸入名稱",
"enter_url_or_path_hint": "輸入一個該清單之網址或絕對的路徑",
@@ -398,7 +401,7 @@
"table_client": "用戶端",
"table_name": "名稱",
"save_btn": "儲存",
"client_add": "增用戶端",
"client_add": "增用戶端",
"client_new": "新的用戶端",
"client_edit": "編輯用戶端",
"client_identifier": "識別碼",
@@ -408,7 +411,7 @@
"form_enter_subnet_ip": "在子網路 \"{{cidr}}\" 中輸入一組 IP 位址",
"form_enter_mac": "輸入媒體存取控制MAC",
"form_enter_id": "輸入識別碼",
"form_add_id": "增識別碼",
"form_add_id": "增識別碼",
"form_client_name": "輸入用戶端名稱",
"name": "名稱",
"client_global_settings": "使用全域的設定",
@@ -423,9 +426,9 @@
"access_title": "存取設定",
"access_desc": "於此您可配置用於 AdGuard Home DNS 伺服器之存取規則。",
"access_allowed_title": "已允許的用戶端",
"access_allowed_desc": "無類別網域間路由CIDRIP 位址之清單。如果被配置AdGuard Home 將接受僅來自這些 IP 位址的請求。",
"access_allowed_desc": "無類別網域間路由CIDRIP 位址或用戶端 ID 之清單。如果被配置AdGuard Home 將接受僅來自這些用戶端的請求。",
"access_disallowed_title": "未被允許的用戶端",
"access_disallowed_desc": "無類別網域間路由CIDRIP 位址之清單。如果被配置AdGuard Home 將排除來自這些 IP 位址的請求。",
"access_disallowed_desc": "無類別網域間路由CIDRIP 位址或用戶端 ID 之清單。如果被配置AdGuard Home 將排除來自這些用戶端的請求。如果已允許的用戶端被配置,此欄位被忽略。",
"access_blocked_title": "未被允許的網域",
"access_blocked_desc": "不要把這個和過濾器混淆。AdGuard Home 排除與這些網域相符的 DNS 查詢且這些查詢甚至不會出現在查詢記錄中。您可相應地明確指定確切的域名、萬用字元wildcard或網址過濾器的規則例如\"example.org\"、\"*.example.org\" 或 \"||example.org^\"。",
"access_settings_saved": "存取設定被成功地儲存",
@@ -436,10 +439,10 @@
"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>這裡是您可使用的軟體之清單。</0>",
"setup_dns_privacy_4": "於 iOS 14 或 macOS Big Sur 裝置上,您可下載增 <highlight>DNS-over-HTTPS</highlight> 或 <highlight>DNS-over-TLS</highlight> 伺服器到 DNS 設定之特殊的 '.mobileconfig' 檔案。",
"setup_dns_privacy_4": "於 iOS 14 或 macOS Big Sur 裝置上,您可下載增 <highlight>DNS-over-HTTPS</highlight> 或 <highlight>DNS-over-TLS</highlight> 伺服器到 DNS 設定之特殊的 '.mobileconfig' 檔案。",
"setup_dns_privacy_android_1": "Android 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> 對 Android 增 <1>DNS-over-HTTPS</1> 支援。",
"setup_dns_privacy_android_3": "<0>Intra</0> 對 Android 增 <1>DNS-over-HTTPS</1> 支援。",
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> 支援 <1>DNS-over-HTTPS</1>,但為了配置它以使用您自己的伺服器,您將需要為它產生一個 <2>DNS 戳記</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": "其它的執行",
@@ -452,7 +455,7 @@
"setup_dns_notice": "為了使用 <1>DNS-over-HTTPS</1> 或 <1>DNS-over-TLS</1>,您需要在 AdGuard Home 設定裡<0>配置加密</0>。",
"rewrite_added": "對於 \"{{key}}\" 之 DNS 改寫被成功地加入",
"rewrite_deleted": "對於 \"{{key}}\" 之 DNS 改寫被成功地刪除",
"rewrite_add": "增 DNS 改寫",
"rewrite_add": "增 DNS 改寫",
"rewrite_not_found": "無已發現之 DNS 改寫",
"rewrite_confirm_delete": "您確定您想要刪除對於 \"{{key}}\" 之 DNS 改寫嗎?",
"rewrite_desc": "允許輕易地配置自訂的 DNS 回應供特定的域名。",
@@ -481,10 +484,12 @@
"encryption_key_source_content": "貼上該私密金鑰內容",
"stats_params": "統計資料配置",
"config_successfully_saved": "配置被成功地儲存",
"interval_6_hour": "6 小時",
"interval_24_hour": "24 小時",
"interval_days": "{{count}} 日",
"interval_days_plural": "{{count}} 日",
"domain": "網域",
"punycode": "國際化域名代碼Punycode",
"answer": "回應",
"filter_added_successfully": "該清單已被成功地加入",
"filter_removed_successfully": "該清單已被成功地移除",
@@ -524,7 +529,7 @@
"example_rewrite_domain": "僅對於此域名改寫回應。",
"example_rewrite_wildcard": "對於所有的 <0>example.org</0> 子網域改寫回應。",
"rewrite_ip_address": "IP 位址:在一個 A 或 AAAA 回應中使用此 IP",
"rewrite_domain_name": "域名:增一筆正規名稱CNAME記錄",
"rewrite_domain_name": "域名:增一筆正規名稱CNAME記錄",
"rewrite_A": "<0>A</0>:特殊的數值,阻止 <0>A</0> 記錄免於該上游",
"rewrite_AAAA": "<0>AAAA</0>:特殊的數值,阻止 <0>AAAA</0> 記錄免於該上游",
"disable_ipv6": "禁用 IPv6 位址之解析",
@@ -543,7 +548,7 @@
"form_enter_host": "輸入主機名稱",
"filtered_custom_rules": "被自訂的過濾規則過濾",
"choose_from_list": "從該清單中選擇",
"add_custom_list": "增一個自訂的清單",
"add_custom_list": "增一個自訂的清單",
"host_whitelisted": "該主機被允許",
"check_ip": "IP 位址:{{ip}}",
"check_cname": "正規名稱CNAME{{cname}}",

View File

@@ -547,6 +547,22 @@ export const resetDhcp = () => async (dispatch) => {
}
};
export const resetDhcpLeasesRequest = createAction('RESET_DHCP_LEASES_REQUEST');
export const resetDhcpLeasesSuccess = createAction('RESET_DHCP_LEASES_SUCCESS');
export const resetDhcpLeasesFailure = createAction('RESET_DHCP_LEASES_FAILURE');
export const resetDhcpLeases = () => async (dispatch) => {
dispatch(resetDhcpLeasesRequest());
try {
const status = await apiClient.resetDhcpLeases();
dispatch(resetDhcpLeasesSuccess(status));
dispatch(addSuccessToast('dhcp_reset_leases_success'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(resetDhcpLeasesFailure());
}
};
export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');
export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');

View File

@@ -264,6 +264,8 @@ class Api {
DHCP_RESET = { path: 'dhcp/reset', method: 'POST' };
DHCP_LEASES_RESET = { path: 'dhcp/reset_leases', method: 'POST' };
getDhcpStatus() {
const { path, method } = this.DHCP_STATUS;
return this.makeRequest(path, method);
@@ -315,6 +317,11 @@ class Api {
return this.makeRequest(path, method);
}
resetDhcpLeases() {
const { path, method } = this.DHCP_LEASES_RESET;
return this.makeRequest(path, method);
}
// Installation
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };

View File

@@ -9,7 +9,7 @@ import Card from '../ui/Card';
import Cell from '../ui/Cell';
import { getPercent, sortIp } from '../../helpers/helpers';
import { BLOCK_ACTIONS, R_CLIENT_ID, STATUS_COLORS } from '../../helpers/constants';
import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats';
@@ -35,10 +35,6 @@ const CountCell = (row) => {
};
const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
if (R_CLIENT_ID.test(ip)) {
return null;
}
const dispatch = useDispatch();
const { t } = useTranslation();
const processingSet = useSelector((state) => state.access.processingSet);

View File

@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { withTranslation, Trans } from 'react-i18next';
import StatsCard from './StatsCard';
import { getPercent, normalizeHistory } from '../../helpers/helpers';
import { RESPONSE_FILTER } from '../../helpers/constants';
const getNormalizedHistory = (data, interval, id) => [
{ data: normalizeHistory(data, interval), id },
@@ -25,7 +27,7 @@ const Statistics = ({
<StatsCard
total={numDnsQueries}
lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')}
title={<Trans>dns_query</Trans>}
title={<Link to="logs"><Trans>dns_query</Trans></Link>}
color="blue"
/>
</div>
@@ -34,7 +36,7 @@ const Statistics = ({
total={numBlockedFiltering}
lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')}
percent={getPercent(numDnsQueries, numBlockedFiltering)}
title={<Trans components={[<a href="#filters" key="0">link</a>]}>blocked_by</Trans>}
title={<Trans components={[<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">link</Link>]}>blocked_by</Trans>}
color="red"
/>
</div>
@@ -47,7 +49,7 @@ const Statistics = ({
'replacedSafebrowsing',
)}
percent={getPercent(numDnsQueries, numReplacedSafebrowsing)}
title={<Trans>stats_malware_phishing</Trans>}
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}><Trans>stats_malware_phishing</Trans></Link>}
color="green"
/>
</div>
@@ -56,7 +58,7 @@ const Statistics = ({
total={numReplacedParental}
lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')}
percent={getPercent(numDnsQueries, numReplacedParental)}
title={<Trans>stats_adult</Trans>}
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}><Trans>stats_adult</Trans></Link>}
color="yellow"
/>
</div>

View File

@@ -16,6 +16,7 @@ const DomainCell = ({
answer_dnssec,
client_proto,
domain,
unicodeName,
time,
tracker,
type,
@@ -41,10 +42,22 @@ const DomainCell = ({
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const ip = type ? `${t('type_table_header')}: ${type}` : '';
const requestDetailsObj = {
let requestDetailsObj = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
domain,
};
if (domain && unicodeName) {
requestDetailsObj = {
...requestDetailsObj,
domain: unicodeName,
punycode: domain,
};
}
requestDetailsObj = {
...requestDetailsObj,
type_table_header: type,
protocol,
};
@@ -54,23 +67,40 @@ const DomainCell = ({
const knownTrackerDataObj = {
name_table_header: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
className="link--green">{sourceData.name}</a>,
source_label: sourceData && (
<a
href={sourceData.url}
target="_blank"
rel="noopener noreferrer"
className="link--green"
>
{sourceData.name}
</a>
),
};
const renderGrid = (content, idx) => {
const preparedContent = typeof content === 'string' ? t(content) : content;
const className = classNames('text-truncate o-hidden', {
'overflow-break': preparedContent.length > 100,
});
return <div key={idx} className={className}>{preparedContent}</div>;
const className = classNames(
'text-truncate o-hidden',
{ 'overflow-break': preparedContent?.length > 100 },
);
return (
<div key={idx} className={className}>
{preparedContent}
</div>
);
};
const getGrid = (contentObj, title, className) => [
<div key={title} className={classNames('pb-2 grid--title', className)}>{t(title)}</div>,
<div key={`${title}-1`}
className="grid grid--limited">{React.Children.map(Object.entries(contentObj), renderGrid)}</div>,
<div key={title} className={classNames('pb-2 grid--title', className)}>
{t(title)}
</div>,
<div key={`${title}-1`} className="grid grid--limited">
{React.Children.map(Object.entries(contentObj), renderGrid)}
</div>,
];
const requestDetails = getGrid(requestDetailsObj, 'request_details');
@@ -81,35 +111,60 @@ const DomainCell = ({
'px-2 d-flex justify-content-center flex-column': isDetailed,
});
const details = [ip, protocol].filter(Boolean)
.join(', ');
const details = [ip, protocol].filter(Boolean).join(', ');
return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
{dnssec_enabled && <IconTooltip
className={lockIconClass}
tooltipClass='py-4 px-5 pb-45'
canShowTooltip={!!answer_dnssec}
xlinkHref='lock'
columnClass='w-100'
content='validated_with_dnssec'
placement='bottom'
/>}
<IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75'
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
place='bottom' />
<div className={valueClass}>
<div className="text-truncate" title={domain}>{domain}</div>
{details && isDetailed
&& <div className="detailed-info d-none d-sm-block text-truncate"
title={details}>{details}</div>}
return (
<div
className="d-flex o-hidden logs__cell logs__cell logs__cell--domain"
role="gridcell"
>
{dnssec_enabled && (
<IconTooltip
className={lockIconClass}
tooltipClass="py-4 px-5 pb-45"
canShowTooltip={!!answer_dnssec}
xlinkHref="lock"
columnClass="w-100"
content="validated_with_dnssec"
placement="bottom"
/>
)}
<IconTooltip
className={privacyIconClass}
tooltipClass="pt-4 pb-5 px-5 mw-75"
xlinkHref="privacy"
contentItemClass="key-colon"
renderContent={renderContent}
place="bottom"
/>
<div className={valueClass}>
{unicodeName ? (
<div className="text-truncate" title={unicodeName}>
{unicodeName}
</div>
) : (
<div className="text-truncate" title={domain}>
{domain}
</div>
)}
{details && isDetailed && (
<div
className="detailed-info d-none d-sm-block text-truncate"
title={details}
>
{details}
</div>
)}
</div>
</div>
</div>;
);
};
DomainCell.propTypes = {
answer_dnssec: propTypes.bool.isRequired,
client_proto: propTypes.string.isRequired,
domain: propTypes.string.isRequired,
unicodeName: propTypes.string,
time: propTypes.string.isRequired,
type: propTypes.string.isRequired,
tracker: propTypes.object,

View File

@@ -21,6 +21,7 @@ import {
getDhcpStatus,
resetDhcp,
setDhcpConfig,
resetDhcpLeases,
toggleDhcp,
toggleLeaseModal,
} from '../../../actions';
@@ -111,6 +112,12 @@ const Dhcp = () => {
}));
};
const handleReset = () => {
if (window.confirm(t('dhcp_reset_leases_confirm'))) {
dispatch(resetDhcpLeases());
}
};
const enteredSomeV4Value = Object.values(v4)
.some(Boolean);
const enteredSomeV6Value = Object.values(v6)
@@ -188,18 +195,18 @@ const Dhcp = () => {
<PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass="page-title--dhcp">
{toggleDhcpButton}
<button
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}
type="button"
className={statusButtonClass}
onClick={onClick}
disabled={enabled || !interface_name || processingConfig}
>
<Trans>check_dhcp_servers</Trans>
</button>
<button
type="button"
className='btn btn-sm btn-outline-secondary'
disabled={!enteredSomeValue || processingConfig}
onClick={clear}
type="button"
className='btn btn-sm btn-outline-secondary'
disabled={!enteredSomeValue || processingConfig}
onClick={clear}
>
<Trans>reset_settings</Trans>
</button>
@@ -269,16 +276,23 @@ const Dhcp = () => {
processingDeleting={processingDeleting}
cidr={cidr}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
<div className="btn-list mt-2">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={toggleModal}
disabled={disabledLeasesButton}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standard mt-3"
onClick={handleReset}
>
<Trans>dhcp_reset_leases</Trans>
</button>
</div>
</div>
</div>
</Card>

View File

@@ -4,26 +4,33 @@ import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { CheckboxField, renderRadioField, toNumber } from '../../../helpers/form';
import { CheckboxField, renderRadioField, toFloatNumber } from '../../../helpers/form';
import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS } from '../../../helpers/constants';
import '../FormButton.css';
const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => {
const title = interval === 1 ? t('interval_24_hour') : t('interval_days', { count: interval });
const getIntervalTitle = (interval, t) => {
switch (interval) {
case 0.25:
return t('interval_6_hour');
case 1:
return t('interval_24_hour');
default:
return t('interval_days', { count: interval });
}
};
return (
<Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={title}
normalize={toNumber}
disabled={processing}
/>
);
});
const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => (
<Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={getIntervalTitle(interval, t)}
normalize={toNumber}
disabled={processing}
/>
));
const Form = (props) => {
const {
@@ -56,7 +63,7 @@ const Form = (props) => {
</label>
<div className="form__group form__group--settings">
<div className="custom-controls-stacked">
{getIntervalFields(processing, t, toNumber)}
{getIntervalFields(processing, t, toFloatNumber)}
</div>
</div>
<div className="mt-5">

View File

@@ -8,22 +8,27 @@ import { renderRadioField, toNumber } from '../../../helpers/form';
import { FORM_NAME, STATS_INTERVALS_DAYS } from '../../../helpers/constants';
import '../FormButton.css';
const getIntervalFields = (processing, t, toNumber) => STATS_INTERVALS_DAYS.map((interval) => {
const title = interval === 1 ? t('interval_24_hour') : t('interval_days', { count: interval });
const getIntervalTitle = (interval, t) => {
switch (interval) {
case 0:
return t('disabled');
case 1:
return t('interval_24_hour');
default:
return t('interval_days', { count: interval });
}
};
return (
<Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={title}
normalize={toNumber}
disabled={processing}
/>
);
});
const getIntervalFields = (processing, t, toNumber) => STATS_INTERVALS_DAYS.map((interval) => <Field
key={interval}
name="interval"
type="radio"
component={renderRadioField}
value={interval}
placeholder={getIntervalTitle(interval, t)}
normalize={toNumber}
disabled={processing}
/>);
const Form = (props) => {
const {

View File

@@ -1,7 +0,0 @@
.stats__link {
color: inherit;
}
.stats__link:hover {
cursor: pointer;
}

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import './LogsSearchLink.css';
import { getLogsUrlParams } from '../../helpers/helpers';
import { MENU_URLS } from '../../helpers/constants';
@@ -13,11 +12,16 @@ const LogsSearchLink = ({
const to = link === MENU_URLS.logs ? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}` : link;
return <Link to={to}
className={'stats__link'}
tabIndex={0}
title={t('click_to_view_queries')}
aria-label={t('click_to_view_queries')}>{children}</Link>;
return (
<Link
to={to}
tabIndex={0}
title={t('click_to_view_queries')}
aria-label={t('click_to_view_queries')}
>
{children}
</Link>
);
};
LogsSearchLink.propTypes = {

View File

@@ -355,9 +355,9 @@ export const ENCRYPTION_SOURCE = {
export const FILTERED = 'Filtered';
export const NOT_FILTERED = 'NotFiltered';
export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
export const STATS_INTERVALS_DAYS = [0, 1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [0.25, 1, 7, 30, 90];
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];

View File

@@ -276,6 +276,12 @@ export const ip4ToInt = (ip) => {
*/
export const toNumber = (value) => value && parseInt(value, 10);
/**
* @param value {string}
* @returns {*|number}
*/
export const toFloatNumber = (value) => value && parseFloat(value, 10);
/**
* @param value {string}
* @returns {boolean}

View File

@@ -77,7 +77,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
upstream,
} = log;
const { host: domain, type } = question;
const { name: domain, unicode_name: unicodeName, type } = question;
const processResponse = (data) => (data ? data.map((response) => {
const { value, type, ttl } = response;
@@ -96,6 +96,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
return {
time,
domain,
unicodeName,
type,
response: processResponse(answer),
reason,

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants';
@@ -63,7 +64,7 @@ export const renderFormattedClientCell = (value, info, isDetailed = false, isLog
}
return <div className="logs__text mw-100" title={value}>
{nameContainer}
<Link to={`logs?search=${encodeURIComponent(value)}`}>{nameContainer}</Link>
{whoisContainer}
</div>;
};

View File

@@ -118,6 +118,11 @@ const dhcp = handleActions(
v6: {},
interface_name: '',
}),
[actions.resetDhcpLeasesSuccess]: (state) => ({
...state,
leases: [],
staticLeases: [],
}),
[actions.toggleLeaseModal]: (state) => {
const newState = {

4
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.16
require (
github.com/AdguardTeam/dnsproxy v0.37.6
github.com/AdguardTeam/dnsproxy v0.38.2
github.com/AdguardTeam/golibs v0.8.0
github.com/AdguardTeam/urlfilter v0.14.6
github.com/NYTimes/gziphandler v1.1.1
@@ -15,7 +15,7 @@ require (
github.com/google/renameio v1.0.1
github.com/insomniacslk/dhcp v0.0.0-20210310193751-cfd4d47082c2
github.com/kardianos/service v1.2.0
github.com/lucas-clemente/quic-go v0.20.1
github.com/lucas-clemente/quic-go v0.21.1
github.com/mdlayher/netlink v1.4.0
github.com/mdlayher/raw v0.0.0-20210412142147-51b895745faf // indirect
github.com/miekg/dns v1.1.42

45
go.sum
View File

@@ -9,11 +9,10 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AdguardTeam/dhcp v0.0.0-20210519141215-51808c73c0bf h1:gc042VRSIRSUzZ+Px6xQCRWNJZTaPkomisDfUZmoFNk=
github.com/AdguardTeam/dhcp v0.0.0-20210519141215-51808c73c0bf/go.mod h1:TKl4jN3Voofo4UJIicyNhWGp/nlQqQkFxmwIFTvBkKI=
github.com/AdguardTeam/dnsproxy v0.37.6 h1:qN9KX/fxLIR5TZKtpPHmMx/AUJLAdeXbQ5sjdqHXWW0=
github.com/AdguardTeam/dnsproxy v0.37.6/go.mod h1:xkJWEuTr550gPDmB9azsciKZzSXjf9wMn+Ji54PQ4gE=
github.com/AdguardTeam/dnsproxy v0.38.2 h1:QHxvShAm4GwH0PyRN60xf18+5nAzmbvhPoEvhfVycSA=
github.com/AdguardTeam/dnsproxy v0.38.2/go.mod h1:aNXKNdTyKfgAG2OS712SYSaGIM9AasZsZxfiY4YiR/0=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.8.0 h1:rHo+yIgT2fivFG0yW2Cwk/DPc2+t/Aw6QvzPpiIFre0=
github.com/AdguardTeam/golibs v0.8.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
@@ -40,7 +39,6 @@ github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47/go.mod h1:6YNgTHL
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -52,8 +50,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
@@ -75,25 +71,21 @@ github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200j
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
@@ -140,8 +132,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucas-clemente/quic-go v0.20.1 h1:hb5m76V8QS/8Nw/suHvXqo3BMHAozvIkcnzpJdpanSk=
github.com/lucas-clemente/quic-go v0.20.1/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
github.com/lucas-clemente/quic-go v0.21.1 h1:uuhCcu885TE9u/piPYMChI/yqA1lXfaLUEx8uCMxf8w=
github.com/lucas-clemente/quic-go v0.21.1/go.mod h1:U9kFi5LKbNIlU30dkuM9vxmTxWq4Bvzee/MjBI+07UA=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
@@ -149,6 +141,8 @@ github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9M
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/marten-seemann/qtls-go1-17 v0.1.0-beta.1.2 h1:SficYjyOthSrliKI+EaFuXS6HqSsX3dkY9AqxAAjBjw=
github.com/marten-seemann/qtls-go1-17 v0.1.0-beta.1.2/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
@@ -202,7 +196,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -254,6 +247,7 @@ github.com/u-root/u-root v7.0.0+incompatible h1:u+KSS04pSxJGI5E7WE4Bs9+Zd75QjFv+
github.com/u-root/u-root v7.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -273,9 +267,8 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -300,11 +293,11 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -317,8 +310,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -353,13 +344,14 @@ golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -378,12 +370,11 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -401,23 +392,16 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
@@ -442,7 +426,6 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=

View File

@@ -10,6 +10,24 @@ import (
"golang.org/x/net/idna"
)
// CloneIP returns a clone of an IP address.
func CloneIP(ip net.IP) (clone net.IP) {
if ip != nil && len(ip) == 0 {
return net.IP{}
}
return append(clone, ip...)
}
// CloneMAC returns a clone of a MAC address.
func CloneMAC(mac net.HardwareAddr) (clone net.HardwareAddr) {
if mac != nil && len(mac) == 0 {
return net.HardwareAddr{}
}
return append(clone, mac...)
}
// IPFromAddr returns an IP address from addr. If addr is neither
// a *net.TCPAddr nor a *net.UDPAddr, it returns nil.
func IPFromAddr(addr net.Addr) (ip net.IP) {
@@ -31,6 +49,12 @@ func IsValidHostOuterRune(r rune) (ok bool) {
(r >= '0' && r <= '9')
}
// JoinHostPort is a convinient wrapper for net.JoinHostPort with port of type
// int.
func JoinHostPort(host string, port int) (hostport string) {
return net.JoinHostPort(host, strconv.Itoa(port))
}
// isValidHostRune returns true if r is a valid rune for a hostname label.
func isValidHostRune(r rune) (ok bool) {
return r == '-' || IsValidHostOuterRune(r)

View File

@@ -9,6 +9,26 @@ import (
"github.com/stretchr/testify/require"
)
func TestCloneIP(t *testing.T) {
assert.Equal(t, net.IP(nil), CloneIP(nil))
assert.Equal(t, net.IP{}, CloneIP(net.IP{}))
ip := net.IP{1, 2, 3, 4}
clone := CloneIP(ip)
assert.Equal(t, ip, clone)
assert.NotSame(t, &ip[0], &clone[0])
}
func TestCloneMAC(t *testing.T) {
assert.Equal(t, net.HardwareAddr(nil), CloneMAC(nil))
assert.Equal(t, net.HardwareAddr{}, CloneMAC(net.HardwareAddr{}))
mac := net.HardwareAddr{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}
clone := CloneMAC(mac)
assert.Equal(t, mac, clone)
assert.NotSame(t, &mac[0], &clone[0])
}
func TestIPFromAddr(t *testing.T) {
ip := net.IP{1, 2, 3, 4}
assert.Equal(t, net.IP(nil), IPFromAddr(nil))
@@ -66,6 +86,14 @@ func TestValidateHardwareAddress(t *testing.T) {
}
}
func TestJoinHostPort(t *testing.T) {
assert.Equal(t, ":0", JoinHostPort("", 0))
assert.Equal(t, "host:12345", JoinHostPort("host", 12345))
assert.Equal(t, "1.2.3.4:12345", JoinHostPort("1.2.3.4", 12345))
assert.Equal(t, "[1234::5678]:12345", JoinHostPort("1234::5678", 12345))
assert.Equal(t, "[1234::5678%lo]:12345", JoinHostPort("1234::5678%lo", 12345))
}
func repeatStr(b *strings.Builder, s string, n int) {
for i := 0; i < n; i++ {
_, _ = b.WriteString(s)

View File

@@ -27,10 +27,9 @@ type EtcHostsContainer struct {
lock sync.RWMutex
// table is the host-to-IPs map.
table map[string][]net.IP
// tableReverse is the IP-to-hosts map.
//
// TODO(a.garipov): Make better use of newtypes. Perhaps a custom map.
tableReverse map[string][]string
// tableReverse is the IP-to-hosts map. The type of the values in the
// map is []string.
tableReverse *IPMap
hostsFn string // path to the main hosts-file
hostsDirs []string // paths to OS-specific directories with hosts-files
@@ -80,7 +79,7 @@ func (ehc *EtcHostsContainer) Init(hostsFn string) {
var err error
ehc.watcher, err = fsnotify.NewWatcher()
if err != nil {
log.Error("etchostscontainer: %s", err)
log.Error("etchosts: %s", err)
}
}
@@ -141,7 +140,7 @@ func (ehc *EtcHostsContainer) Process(host string, qtype uint16) []net.IP {
copy(ipsCopy, ips)
}
log.Debug("etchostscontainer: answer: %s -> %v", host, ipsCopy)
log.Debug("etchosts: answer: %s -> %v", host, ipsCopy)
return ipsCopy
}
@@ -151,38 +150,40 @@ func (ehc *EtcHostsContainer) ProcessReverse(addr string, qtype uint16) (hosts [
return nil
}
ipReal := UnreverseAddr(addr)
if ipReal == nil {
ip := UnreverseAddr(addr)
if ip == nil {
return nil
}
ipStr := ipReal.String()
ehc.lock.RLock()
defer ehc.lock.RUnlock()
hosts = ehc.tableReverse[ipStr]
if len(hosts) == 0 {
return nil // not found
v, ok := ehc.tableReverse.Get(ip)
if !ok {
return nil
}
log.Debug("etchostscontainer: reverse-lookup: %s -> %s", addr, hosts)
hosts, ok = v.([]string)
if !ok {
log.Error("etchosts: bad type %T in tableReverse for %s", v, ip)
return nil
} else if len(hosts) == 0 {
return nil
}
log.Debug("etchosts: reverse-lookup: %s -> %s", addr, hosts)
return hosts
}
// List returns an IP-to-hostnames table. It is safe for concurrent use.
func (ehc *EtcHostsContainer) List() (ipToHosts map[string][]string) {
// List returns an IP-to-hostnames table. The type of the values in the map is
// []string. It is safe for concurrent use.
func (ehc *EtcHostsContainer) List() (ipToHosts *IPMap) {
ehc.lock.RLock()
defer ehc.lock.RUnlock()
ipToHosts = make(map[string][]string, len(ehc.tableReverse))
for k, v := range ehc.tableReverse {
ipToHosts[k] = v
}
return ipToHosts
return ehc.tableReverse.ShallowClone()
}
// update table
@@ -205,29 +206,31 @@ func (ehc *EtcHostsContainer) updateTable(table map[string][]net.IP, host string
ok = true
}
if ok {
log.Debug("etchostscontainer: added %s -> %s", ipAddr, host)
log.Debug("etchosts: added %s -> %s", ipAddr, host)
}
}
// updateTableRev updates the reverse address table.
func (ehc *EtcHostsContainer) updateTableRev(tableRev map[string][]string, newHost string, ipAddr net.IP) {
ipStr := ipAddr.String()
hosts, ok := tableRev[ipStr]
func (ehc *EtcHostsContainer) updateTableRev(tableRev *IPMap, newHost string, ip net.IP) {
v, ok := tableRev.Get(ip)
if !ok {
tableRev[ipStr] = []string{newHost}
log.Debug("etchostscontainer: added reverse-address %s -> %s", ipStr, newHost)
tableRev.Set(ip, []string{newHost})
log.Debug("etchosts: added reverse-address %s -> %s", ip, newHost)
return
}
hosts, _ := v.([]string)
for _, host := range hosts {
if host == newHost {
return
}
}
tableRev[ipStr] = append(tableRev[ipStr], newHost)
log.Debug("etchostscontainer: added reverse-address %s -> %s", ipStr, newHost)
hosts = append(hosts, newHost)
tableRev.Set(ip, hosts)
log.Debug("etchosts: added reverse-address %s -> %s", ip, newHost)
}
// parseHostsLine parses hosts from the fields.
@@ -255,12 +258,12 @@ func parseHostsLine(fields []string) (hosts []string) {
// line for one IP are supported.
func (ehc *EtcHostsContainer) load(
table map[string][]net.IP,
tableRev map[string][]string,
tableRev *IPMap,
fn string,
) {
f, err := os.Open(fn)
if err != nil {
log.Error("etchostscontainer: %s", err)
log.Error("etchosts: %s", err)
return
}
@@ -268,11 +271,11 @@ func (ehc *EtcHostsContainer) load(
defer func() {
derr := f.Close()
if derr != nil {
log.Error("etchostscontainer: closing file: %s", err)
log.Error("etchosts: closing file: %s", err)
}
}()
log.Debug("etchostscontainer: loading hosts from file %s", fn)
log.Debug("etchosts: loading hosts from file %s", fn)
s := bufio.NewScanner(f)
for s.Scan() {
@@ -296,7 +299,7 @@ func (ehc *EtcHostsContainer) load(
err = s.Err()
if err != nil {
log.Error("etchostscontainer: %s", err)
log.Error("etchosts: %s", err)
}
}
@@ -334,7 +337,7 @@ func (ehc *EtcHostsContainer) watcherLoop() {
}
if event.Op&fsnotify.Write == fsnotify.Write {
log.Debug("etchostscontainer: modified: %s", event.Name)
log.Debug("etchosts: modified: %s", event.Name)
ehc.updateHosts()
}
@@ -342,7 +345,7 @@ func (ehc *EtcHostsContainer) watcherLoop() {
if !ok {
return
}
log.Error("etchostscontainer: %s", err)
log.Error("etchosts: %s", err)
}
}
}
@@ -350,7 +353,7 @@ func (ehc *EtcHostsContainer) watcherLoop() {
// updateHosts - loads system hosts
func (ehc *EtcHostsContainer) updateHosts() {
table := make(map[string][]net.IP)
tableRev := make(map[string][]string)
tableRev := NewIPMap(0)
ehc.load(table, tableRev, ehc.hostsFn)
@@ -358,7 +361,7 @@ func (ehc *EtcHostsContainer) updateHosts() {
des, err := os.ReadDir(dir)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Error("etchostscontainer: Opening directory: %q: %s", dir, err)
log.Error("etchosts: Opening directory: %q: %s", dir, err)
}
continue

View File

@@ -70,7 +70,7 @@ func TestEtcHostsContainerResolution(t *testing.T) {
})
t.Run("hosts_file", func(t *testing.T) {
names, ok := ehc.List()["127.0.0.1"]
names, ok := ehc.List().Get(net.IP{127, 0, 0, 1})
require.True(t, ok)
assert.Equal(t, []string{"host", "localhost"}, names)
})

112
internal/aghnet/ipmap.go Normal file
View File

@@ -0,0 +1,112 @@
package aghnet
import (
"fmt"
"net"
)
// ipArr is a representation of an IP address as an array of bytes.
type ipArr [16]byte
// String implements the fmt.Stringer interface for ipArr.
func (a ipArr) String() (s string) {
return net.IP(a[:]).String()
}
// IPMap is a map of IP addresses.
type IPMap struct {
m map[ipArr]interface{}
}
// NewIPMap returns a new empty IP map using hint as a size hint for the
// underlying map.
func NewIPMap(hint int) (m *IPMap) {
return &IPMap{
m: make(map[ipArr]interface{}, hint),
}
}
// ipToArr converts a net.IP into an ipArr.
//
// TODO(a.garipov): Use the slice-to-array conversion in Go 1.17.
func ipToArr(ip net.IP) (a ipArr) {
copy(a[:], ip.To16())
return a
}
// Del deletes ip from the map. Calling Del on a nil *IPMap has no effect, just
// like delete on an empty map doesn't.
func (m *IPMap) Del(ip net.IP) {
if m != nil {
delete(m.m, ipToArr(ip))
}
}
// Get returns the value from the map. Calling Get on a nil *IPMap returns nil
// and false, just like indexing on an empty map does.
func (m *IPMap) Get(ip net.IP) (v interface{}, ok bool) {
if m != nil {
v, ok = m.m[ipToArr(ip)]
return v, ok
}
return nil, false
}
// Len returns the length of the map. A nil *IPMap has a length of zero, just
// like an empty map.
func (m *IPMap) Len() (n int) {
if m == nil {
return 0
}
return len(m.m)
}
// Range calls f for each key and value present in the map in an undefined
// order. If cont is false, range stops the iteration. Calling Range on a nil
// *IPMap has no effect, just like ranging over a nil map.
func (m *IPMap) Range(f func(ip net.IP, v interface{}) (cont bool)) {
if m == nil {
return
}
for k, v := range m.m {
if !f(net.IP(k[:]), v) {
break
}
}
}
// Set sets the value. Set panics if the m is a nil *IPMap, just like a nil map
// does.
func (m *IPMap) Set(ip net.IP, v interface{}) {
m.m[ipToArr(ip)] = v
}
// ShallowClone returns a shallow clone of the map.
func (m *IPMap) ShallowClone() (sclone *IPMap) {
if m == nil {
return nil
}
sclone = NewIPMap(m.Len())
m.Range(func(ip net.IP, v interface{}) (cont bool) {
sclone.Set(ip, v)
return true
})
return sclone
}
// String implements the fmt.Stringer interface for *IPMap.
func (m *IPMap) String() (s string) {
if m == nil {
return "<nil>"
}
return fmt.Sprint(m.m)
}

View File

@@ -0,0 +1,142 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIPMap_allocs(t *testing.T) {
ip4 := net.IP{1, 2, 3, 4}
m := NewIPMap(0)
m.Set(ip4, 42)
t.Run("get", func(t *testing.T) {
var v interface{}
var ok bool
allocs := testing.AllocsPerRun(100, func() {
v, ok = m.Get(ip4)
})
require.True(t, ok)
require.Equal(t, 42, v)
assert.Equal(t, float64(0), allocs)
})
t.Run("len", func(t *testing.T) {
var n int
allocs := testing.AllocsPerRun(100, func() {
n = m.Len()
})
require.Equal(t, 1, n)
assert.Equal(t, float64(0), allocs)
})
}
func TestIPMap(t *testing.T) {
ip4 := net.IP{1, 2, 3, 4}
ip6 := net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x56, 0x78,
}
val := 42
t.Run("nil", func(t *testing.T) {
var m *IPMap
assert.NotPanics(t, func() {
m.Del(ip4)
m.Del(ip6)
})
assert.NotPanics(t, func() {
v, ok := m.Get(ip4)
assert.Nil(t, v)
assert.False(t, ok)
v, ok = m.Get(ip6)
assert.Nil(t, v)
assert.False(t, ok)
})
assert.NotPanics(t, func() {
assert.Equal(t, 0, m.Len())
})
assert.NotPanics(t, func() {
n := 0
m.Range(func(_ net.IP, _ interface{}) (cont bool) {
n++
return true
})
assert.Equal(t, 0, n)
})
assert.Panics(t, func() {
m.Set(ip4, val)
})
assert.Panics(t, func() {
m.Set(ip6, val)
})
assert.NotPanics(t, func() {
sclone := m.ShallowClone()
assert.Nil(t, sclone)
})
})
testIPMap := func(t *testing.T, ip net.IP, s string) {
m := NewIPMap(0)
assert.Equal(t, 0, m.Len())
v, ok := m.Get(ip)
assert.Nil(t, v)
assert.False(t, ok)
m.Set(ip, val)
v, ok = m.Get(ip)
assert.Equal(t, val, v)
assert.True(t, ok)
n := 0
m.Range(func(ipKey net.IP, v interface{}) (cont bool) {
assert.Equal(t, ip.To16(), ipKey)
assert.Equal(t, val, v)
n++
return false
})
assert.Equal(t, 1, n)
sclone := m.ShallowClone()
assert.Equal(t, m, sclone)
assert.Equal(t, s, m.String())
m.Del(ip)
v, ok = m.Get(ip)
assert.Nil(t, v)
assert.False(t, ok)
assert.Equal(t, 0, m.Len())
}
t.Run("ipv4", func(t *testing.T) {
testIPMap(t, ip4, "map[1.2.3.4:42]")
})
t.Run("ipv6", func(t *testing.T) {
testIPMap(t, ip6, "map[1234::5678:42]")
})
}

26
internal/aghnet/ipset.go Normal file
View File

@@ -0,0 +1,26 @@
package aghnet
import (
"net"
)
// IpsetManager is the ipset manager interface.
//
// TODO(a.garipov): Perhaps generalize this into some kind of a NetFilter type,
// since ipset is exclusive to Linux?
type IpsetManager interface {
Add(host string, ip4s, ip6s []net.IP) (n int, err error)
Close() (err error)
}
// NewIpsetManager returns a new ipset. IPv4 addresses are added to an ipset
// with an ipv4 family; IPv6 addresses, to an ipv6 ipset. ipset must exist.
//
// The syntax of the ipsetConf is:
//
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
//
// The error is of type *aghos.UnsupportedError if the OS is not supported.
func NewIpsetManager(ipsetConf []string) (mgr IpsetManager, err error) {
return newIpsetMgr(ipsetConf)
}

View File

@@ -0,0 +1,376 @@
//go:build linux
// +build linux
package aghnet
import (
"fmt"
"net"
"strings"
"sync"
"github.com/AdguardTeam/golibs/errors"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/ti-mo/netfilter"
)
// How to test on a real Linux machine:
//
// 1. Run:
//
// sudo ipset create example_set hash:ip family ipv4
//
// 2. Run:
//
// sudo ipset list example_set
//
// The Members field should be empty.
//
// 3. Add the line "example.com/example_set" to your AdGuardHome.yaml.
//
// 4. Start AdGuardHome.
//
// 5. Make requests to example.com and its subdomains.
//
// 6. Run:
//
// sudo ipset list example_set
//
// The Members field should contain the resolved IP addresses.
// newIpsetMgr returns a new Linux ipset manager.
func newIpsetMgr(ipsetConf []string) (set IpsetManager, err error) {
dial := func(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error) {
return ipset.Dial(pf, conf)
}
return newIpsetMgrWithDialer(ipsetConf, dial)
}
// ipsetConn is the ipset conn interface.
type ipsetConn interface {
Add(name string, entries ...*ipset.Entry) (err error)
Close() (err error)
Header(name string) (p *ipset.HeaderPolicy, err error)
}
// ipsetDialer creates an ipsetConn.
type ipsetDialer func(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error)
// ipsetProps contains one Linux Netfilter ipset properties.
type ipsetProps struct {
name string
family netfilter.ProtoFamily
}
// unit is a convenient alias for struct{}.
type unit = struct{}
// ipsetMgr is the Linux Netfilter ipset manager.
type ipsetMgr struct {
nameToIpset map[string]ipsetProps
domainToIpsets map[string][]ipsetProps
dial ipsetDialer
// mu protects all properties below.
mu *sync.Mutex
// TODO(a.garipov): Currently, the ipset list is static, and we don't
// read the IPs already in sets, so we can assume that all incoming IPs
// are either added to all corresponding ipsets or not. When that stops
// being the case, for example if we add dynamic reconfiguration of
// ipsets, this map will need to become a per-ipset-name one.
addedIPs map[[16]byte]unit
ipv4Conn ipsetConn
ipv6Conn ipsetConn
}
// dialNetfilter establishes connections to Linux's netfilter module.
func (m *ipsetMgr) dialNetfilter(conf *netlink.Config) (err error) {
// The kernel API does not actually require two sockets but package
// github.com/digineo/go-ipset does.
//
// TODO(a.garipov): Perhaps we can ditch package ipset altogether and
// just use packages netfilter and netlink.
m.ipv4Conn, err = m.dial(netfilter.ProtoIPv4, conf)
if err != nil {
return fmt.Errorf("dialing v4: %w", err)
}
m.ipv6Conn, err = m.dial(netfilter.ProtoIPv6, conf)
if err != nil {
return fmt.Errorf("dialing v6: %w", err)
}
return nil
}
// parseIpsetConfig parses one ipset configuration string.
func parseIpsetConfig(confStr string) (hosts, ipsetNames []string, err error) {
confStr = strings.TrimSpace(confStr)
hostsAndNames := strings.Split(confStr, "/")
if len(hostsAndNames) != 2 {
return nil, nil, fmt.Errorf("invalid value %q: expected one slash", confStr)
}
hosts = strings.Split(hostsAndNames[0], ",")
ipsetNames = strings.Split(hostsAndNames[1], ",")
if len(ipsetNames) == 0 {
return nil, nil, nil
}
for i := range ipsetNames {
ipsetNames[i] = strings.TrimSpace(ipsetNames[i])
if len(ipsetNames[i]) == 0 {
return nil, nil, fmt.Errorf("invalid value %q: empty ipset name", confStr)
}
}
for i := range hosts {
hosts[i] = strings.ToLower(strings.TrimSpace(hosts[i]))
}
return hosts, ipsetNames, nil
}
// ipsetProps returns the properties of an ipset with the given name.
func (m *ipsetMgr) ipsetProps(name string) (set ipsetProps, err error) {
// The family doesn't seem to matter when we use a header query, so
// query only the IPv4 one.
//
// TODO(a.garipov): Find out if this is a bug or a feature.
var res *ipset.HeaderPolicy
res, err = m.ipv4Conn.Header(name)
if err != nil {
return set, err
}
if res == nil || res.Family == nil {
return set, errors.Error("empty response or no family data")
}
family := netfilter.ProtoFamily(res.Family.Value)
if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {
return set, fmt.Errorf("unexpected ipset family %d", family)
}
return ipsetProps{
name: name,
family: family,
}, nil
}
// ipsets returns currently known ipsets.
func (m *ipsetMgr) ipsets(names []string) (sets []ipsetProps, err error) {
for _, name := range names {
set, ok := m.nameToIpset[name]
if ok {
sets = append(sets, set)
continue
}
set, err = m.ipsetProps(name)
if err != nil {
return nil, fmt.Errorf("querying ipset %q: %w", name, err)
}
m.nameToIpset[name] = set
sets = append(sets, set)
}
return sets, nil
}
// newIpsetMgrWithDialer returns a new Linux ipset manager using the provided
// dialer.
func newIpsetMgrWithDialer(ipsetConf []string, dial ipsetDialer) (mgr IpsetManager, err error) {
defer func() { err = errors.Annotate(err, "ipset: %w") }()
m := &ipsetMgr{
mu: &sync.Mutex{},
nameToIpset: make(map[string]ipsetProps),
domainToIpsets: make(map[string][]ipsetProps),
dial: dial,
addedIPs: make(map[[16]byte]unit),
}
err = m.dialNetfilter(&netlink.Config{})
if err != nil {
return nil, fmt.Errorf("dialing netfilter: %w", err)
}
for i, confStr := range ipsetConf {
var hosts, ipsetNames []string
hosts, ipsetNames, err = parseIpsetConfig(confStr)
if err != nil {
return nil, fmt.Errorf("config line at idx %d: %w", i, err)
}
var ipsets []ipsetProps
ipsets, err = m.ipsets(ipsetNames)
if err != nil {
return nil, fmt.Errorf(
"getting ipsets from config line at idx %d: %w",
i,
err,
)
}
for _, host := range hosts {
m.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)
}
}
return m, nil
}
// lookupHost find the ipsets for the host, taking subdomain wildcards into
// account.
func (m *ipsetMgr) lookupHost(host string) (sets []ipsetProps) {
// Search for matching ipset hosts starting with most specific domain.
// We could use a trie here but the simple, inefficient solution isn't
// that expensive: ~10 ns for TLD + SLD vs. ~140 ns for 10 subdomains on
// an AMD Ryzen 7 PRO 4750U CPU; ~120 ns vs. ~ 1500 ns on a Raspberry
// Pi's ARMv7 rev 4 CPU.
for i := 0; ; i++ {
host = host[i:]
sets = m.domainToIpsets[host]
if sets != nil {
return sets
}
i = strings.Index(host, ".")
if i == -1 {
break
}
}
// Check the root catch-all one.
return m.domainToIpsets[""]
}
// addIPs adds the IP addresses for the host to the ipset. set must be same
// family as set's family.
func (m *ipsetMgr) addIPs(host string, set ipsetProps, ips []net.IP) (n int, err error) {
if len(ips) == 0 {
return 0, nil
}
var entries []*ipset.Entry
var newAddedIPs [][16]byte
for _, ip := range ips {
var iparr [16]byte
copy(iparr[:], ip.To16())
if _, added := m.addedIPs[iparr]; added {
continue
}
entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))
newAddedIPs = append(newAddedIPs, iparr)
}
n = len(entries)
if n == 0 {
return 0, nil
}
var conn ipsetConn
switch set.family {
case netfilter.ProtoIPv4:
conn = m.ipv4Conn
case netfilter.ProtoIPv6:
conn = m.ipv6Conn
default:
return 0, fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
}
err = conn.Add(set.name, entries...)
if err != nil {
return 0, fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err)
}
// Only add these to the cache once we're sure that all of them were
// actually sent to the ipset.
for _, iparr := range newAddedIPs {
m.addedIPs[iparr] = unit{}
}
return n, nil
}
// addToSets adds the IP addresses to the corresponding ipset.
func (m *ipsetMgr) addToSets(
host string,
ip4s []net.IP,
ip6s []net.IP,
sets []ipsetProps,
) (n int, err error) {
for _, set := range sets {
var nn int
switch set.family {
case netfilter.ProtoIPv4:
nn, err = m.addIPs(host, set, ip4s)
if err != nil {
return n, err
}
case netfilter.ProtoIPv6:
nn, err = m.addIPs(host, set, ip6s)
if err != nil {
return n, err
}
default:
return n, fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
}
n += nn
}
return n, nil
}
// Add implements the IpsetManager interface for *ipsetMgr
func (m *ipsetMgr) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
sets := m.lookupHost(host)
if len(sets) == 0 {
return 0, nil
}
return m.addToSets(host, ip4s, ip6s, sets)
}
// Close implements the IpsetManager interface for *ipsetMgr.
func (m *ipsetMgr) Close() (err error) {
m.mu.Lock()
defer m.mu.Unlock()
var errs []error
// Close both and collect errors so that the errors from closing one
// don't interfere with closing the other.
err = m.ipv4Conn.Close()
if err != nil {
errs = append(errs, err)
}
err = m.ipv6Conn.Close()
if err != nil {
errs = append(errs, err)
}
if len(errs) != 0 {
return errors.List("closing ipsets", errs...)
}
return nil
}

View File

@@ -0,0 +1,155 @@
//go:build linux
// +build linux
package aghnet
import (
"net"
"strings"
"testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ti-mo/netfilter"
)
// fakeIpsetConn is a fake ipsetConn for tests.
type fakeIpsetConn struct {
ipv4Header *ipset.HeaderPolicy
ipv4Entries *[]*ipset.Entry
ipv6Header *ipset.HeaderPolicy
ipv6Entries *[]*ipset.Entry
}
// Add implements the ipsetConn interface for *fakeIpsetConn.
func (c *fakeIpsetConn) Add(name string, entries ...*ipset.Entry) (err error) {
if strings.Contains(name, "ipv4") {
*c.ipv4Entries = append(*c.ipv4Entries, entries...)
return nil
} else if strings.Contains(name, "ipv6") {
*c.ipv6Entries = append(*c.ipv6Entries, entries...)
return nil
}
return errors.Error("test: ipset not found")
}
// Close implements the ipsetConn interface for *fakeIpsetConn.
func (c *fakeIpsetConn) Close() (err error) {
return nil
}
// Header implements the ipsetConn interface for *fakeIpsetConn.
func (c *fakeIpsetConn) Header(name string) (p *ipset.HeaderPolicy, err error) {
if strings.Contains(name, "ipv4") {
return c.ipv4Header, nil
} else if strings.Contains(name, "ipv6") {
return c.ipv6Header, nil
}
return nil, errors.Error("test: ipset not found")
}
func TestIpsetMgr_Add(t *testing.T) {
ipsetConf := []string{
"example.com,example.net/ipv4set",
"example.org,example.biz/ipv6set",
}
var ipv4Entries []*ipset.Entry
var ipv6Entries []*ipset.Entry
fakeDial := func(
pf netfilter.ProtoFamily,
conf *netlink.Config,
) (conn ipsetConn, err error) {
return &fakeIpsetConn{
ipv4Header: &ipset.HeaderPolicy{
Family: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv4)),
},
ipv4Entries: &ipv4Entries,
ipv6Header: &ipset.HeaderPolicy{
Family: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv6)),
},
ipv6Entries: &ipv6Entries,
}, nil
}
m, err := newIpsetMgrWithDialer(ipsetConf, fakeDial)
require.NoError(t, err)
ip4 := net.IP{1, 2, 3, 4}
ip6 := net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x56, 0x78,
}
n, err := m.Add("example.net", []net.IP{ip4}, nil)
require.NoError(t, err)
assert.Equal(t, 1, n)
require.Len(t, ipv4Entries, 1)
gotIP4 := ipv4Entries[0].IP.Value
assert.Equal(t, ip4, gotIP4)
n, err = m.Add("example.biz", nil, []net.IP{ip6})
require.NoError(t, err)
assert.Equal(t, 1, n)
require.Len(t, ipv6Entries, 1)
gotIP6 := ipv6Entries[0].IP.Value
assert.Equal(t, ip6, gotIP6)
err = m.Close()
assert.NoError(t, err)
}
var ipsetPropsSink []ipsetProps
func BenchmarkIpsetMgr_lookupHost(b *testing.B) {
propsLong := []ipsetProps{{
name: "example.com",
family: netfilter.ProtoIPv4,
}}
propsShort := []ipsetProps{{
name: "example.net",
family: netfilter.ProtoIPv4,
}}
m := &ipsetMgr{
domainToIpsets: map[string][]ipsetProps{
"": propsLong,
"example.net": propsShort,
},
}
b.Run("long", func(b *testing.B) {
const name = "a.very.long.domain.name.inside.the.domain.example.com"
for i := 0; i < b.N; i++ {
ipsetPropsSink = m.lookupHost(name)
}
require.Equal(b, propsLong, ipsetPropsSink)
})
b.Run("short", func(b *testing.B) {
const name = "example.net"
for i := 0; i < b.N; i++ {
ipsetPropsSink = m.lookupHost(name)
}
require.Equal(b, propsShort, ipsetPropsSink)
})
}

View File

@@ -0,0 +1,12 @@
//go:build !linux
// +build !linux
package aghnet
import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
)
func newIpsetMgr(_ []string) (mgr IpsetManager, err error) {
return nil, aghos.Unsupported("ipset")
}

View File

@@ -71,6 +71,12 @@ func CanBindPort(port int) (can bool, err error) {
return true, nil
}
// CanBindPrivilegedPorts checks if current process can bind to privileged
// ports.
func CanBindPrivilegedPorts() (can bool, err error) {
return canBindPrivilegedPorts()
}
// NetInterface represents an entry of network interfaces map.
type NetInterface struct {
MTU int `json:"mtu"`
@@ -424,9 +430,3 @@ func CollectAllIfacesAddrs() (addrs []string, err error) {
return addrs, nil
}
// JoinHostPort is a convinient wrapper for net.JoinHostPort with port of type
// int.
func JoinHostPort(host string, port int) (hostport string) {
return net.JoinHostPort(host, strconv.Itoa(port))
}

View File

@@ -13,8 +13,8 @@ import (
"github.com/AdguardTeam/golibs/errors"
)
// hardwarePortInfo - information obtained using MacOS networksetup
// about the current state of the internet connection
// hardwarePortInfo contains information about the current state of the internet
// connection obtained from macOS networksetup.
type hardwarePortInfo struct {
name string
ip string
@@ -23,6 +23,10 @@ type hardwarePortInfo struct {
static bool
}
func canBindPrivilegedPorts() (can bool, err error) {
return aghos.HaveAdminRights()
}
func ifaceHasStaticIP(ifaceName string) (bool, error) {
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
if err != nil {

View File

@@ -9,16 +9,132 @@ import (
"io"
"net"
"os"
"path/filepath"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/errors"
"github.com/google/renameio/maybe"
"golang.org/x/sys/unix"
)
// maxConfigFileSize is the maximum assumed length of the interface
// configuration file.
const maxConfigFileSize = 1024 * 1024
// recurrentChecker is used to check all the files which may include references
// for other ones.
type recurrentChecker struct {
// checker is the function to check if r's stream contains the desired
// attribute. It must return all the patterns for files which should
// also be checked and each of them should be valid for filepath.Glob
// function.
checker func(r io.Reader, desired string) (patterns []string, has bool, err error)
// initPath is the path of the first member in the sequence of checked
// files.
initPath string
}
// maxCheckedFileSize is the maximum length of the file that recurrentChecker
// may check.
const maxCheckedFileSize = 1024 * 1024
// checkFile tries to open and to check single file located on the sourcePath.
func (rc *recurrentChecker) checkFile(sourcePath, desired string) (
subsources []string,
has bool,
err error,
) {
var f *os.File
f, err = os.Open(sourcePath)
if err != nil {
return nil, false, err
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
var r io.Reader
r, err = aghio.LimitReader(f, maxCheckedFileSize)
if err != nil {
return nil, false, err
}
subsources, has, err = rc.checker(r, desired)
if err != nil {
return nil, false, err
}
if has {
return nil, true, nil
}
return subsources, has, nil
}
// handlePatterns parses the patterns and takes care of duplicates.
func (rc *recurrentChecker) handlePatterns(sourcesSet *aghstrings.Set, patterns []string) (
subsources []string,
err error,
) {
subsources = make([]string, 0, len(patterns))
for _, p := range patterns {
var matches []string
matches, err = filepath.Glob(p)
if err != nil {
return nil, fmt.Errorf("invalid pattern %q: %w", p, err)
}
for _, m := range matches {
if sourcesSet.Has(m) {
continue
}
sourcesSet.Add(m)
subsources = append(subsources, m)
}
}
return subsources, nil
}
// check walks through all the files searching for the desired attribute.
func (rc *recurrentChecker) check(desired string) (has bool, err error) {
var i int
sources := []string{rc.initPath}
defer func() {
if i >= len(sources) {
return
}
err = errors.Annotate(err, "checking %q: %w", sources[i])
}()
var patterns, subsources []string
// The slice of sources is separate from the set of sources to keep the
// order in which the files are walked.
for sourcesSet := aghstrings.NewSet(rc.initPath); i < len(sources); i++ {
patterns, has, err = rc.checkFile(sources[i], desired)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return false, err
}
if has {
return true, nil
}
subsources, err = rc.handlePatterns(sourcesSet, patterns)
if err != nil {
return false, err
}
sources = append(sources, subsources...)
}
return false, nil
}
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
// TODO(a.garipov): Currently, this function returns the first
@@ -26,48 +142,34 @@ func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
// /etc/network/interfaces doesn't, it will return true. Perhaps this
// is not the most desirable behavior.
for _, check := range []struct {
checker func(io.Reader, string) (bool, error)
filePath string
}{{
for _, rc := range []*recurrentChecker{{
checker: dhcpcdStaticConfig,
filePath: "/etc/dhcpcd.conf",
initPath: "/etc/dhcpcd.conf",
}, {
checker: ifacesStaticConfig,
filePath: "/etc/network/interfaces",
initPath: "/etc/network/interfaces",
}} {
var f *os.File
f, err = os.Open(check.filePath)
if err != nil {
// ErrNotExist can happen here if there is no such file.
// This is normal, as not every system uses those files.
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
}
return false, err
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
var fileReader io.Reader
fileReader, err = aghio.LimitReader(f, maxConfigFileSize)
has, err = rc.check(ifaceName)
if err != nil {
return false, err
}
has, err = check.checker(fileReader, ifaceName)
if err != nil {
return false, err
if has {
return true, nil
}
return has, nil
}
return false, ErrNoStaticIPInfo
}
func canBindPrivilegedPorts() (can bool, err error) {
cnbs, err := unix.PrctlRetInt(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, unix.CAP_NET_BIND_SERVICE, 0, 0)
// Don't check the error because it's always nil on Linux.
adm, _ := aghos.HaveAdminRights()
return cnbs == 1 || adm, err
}
// findIfaceLine scans s until it finds the line that declares an interface with
// the given name. If findIfaceLine can't find the line, it returns false.
func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
@@ -84,11 +186,11 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to
// have a static IP.
func dhcpcdStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
func dhcpcdStaticConfig(r io.Reader, ifaceName string) (subsources []string, has bool, err error) {
s := bufio.NewScanner(r)
ifaceFound := findIfaceLine(s, ifaceName)
if !ifaceFound {
return false, s.Err()
return nil, false, s.Err()
}
for s.Scan() {
@@ -97,7 +199,7 @@ func dhcpcdStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
if len(fields) >= 2 &&
fields[0] == "static" &&
strings.HasPrefix(fields[1], "ip_address=") {
return true, s.Err()
return nil, true, s.Err()
}
if len(fields) > 0 && fields[0] == "interface" {
@@ -106,29 +208,41 @@ func dhcpcdStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
}
}
return false, s.Err()
return nil, false, s.Err()
}
// ifacesStaticConfig checks if interface is configured by
// /etc/network/interfaces to have a static IP.
func ifacesStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
// ifacesStaticConfig checks if the interface is configured by any file of
// /etc/network/interfaces format to have a static IP.
func ifacesStaticConfig(r io.Reader, ifaceName string) (subsources []string, has bool, err error) {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if len(line) == 0 || line[0] == '#' {
if aghstrings.IsCommentOrEmpty(line) {
continue
}
// TODO(e.burkov): As man page interfaces(5) says, a line may be
// extended across multiple lines by making the last character a
// backslash. Provide extended lines and "source-directory"
// stanzas support.
fields := strings.Fields(line)
fieldsNum := len(fields)
// Man page interfaces(5) declares that interface definition
// should consist of the key word "iface" followed by interface
// name, and method at fourth field.
if len(fields) >= 4 && fields[0] == "iface" && fields[1] == ifaceName && fields[3] == "static" {
return true, nil
if fieldsNum >= 4 &&
fields[0] == "iface" && fields[1] == ifaceName && fields[3] == "static" {
return nil, true, nil
}
if fieldsNum >= 2 && fields[0] == "source" {
subsources = append(subsources, fields[1])
}
}
return false, s.Err()
return subsources, false, s.Err()
}
// ifaceSetStaticIP configures the system to retain its current IP on the

View File

@@ -12,6 +12,21 @@ import (
"github.com/stretchr/testify/require"
)
func TestRecurrentChecker(t *testing.T) {
c := &recurrentChecker{
checker: ifacesStaticConfig,
initPath: "./testdata/include-subsources",
}
has, err := c.check("sample_name")
require.NoError(t, err)
assert.True(t, has)
has, err = c.check("another_name")
require.NoError(t, err)
assert.False(t, has)
}
const nl = "\n"
func TestDHCPCDStaticConfig(t *testing.T) {
@@ -49,7 +64,7 @@ func TestDHCPCDStaticConfig(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data)
has, err := dhcpcdStaticConfig(r, "wlan0")
_, has, err := dhcpcdStaticConfig(r, "wlan0")
require.NoError(t, err)
assert.Equal(t, tc.want, has)
@@ -59,9 +74,10 @@ func TestDHCPCDStaticConfig(t *testing.T) {
func TestIfacesStaticConfig(t *testing.T) {
testCases := []struct {
name string
data []byte
want bool
name string
data []byte
want bool
wantPatterns []string
}{{
name: "has_not",
data: []byte(`allow-hotplug enp0s3` + nl +
@@ -71,7 +87,8 @@ func TestIfacesStaticConfig(t *testing.T) {
`# gateway 192.168.0.1` + nl +
`iface enp0s3 inet dhcp` + nl,
),
want: false,
want: false,
wantPatterns: []string{},
}, {
name: "has",
data: []byte(`allow-hotplug enp0s3` + nl +
@@ -81,16 +98,36 @@ func TestIfacesStaticConfig(t *testing.T) {
` gateway 192.168.0.1` + nl +
`#iface enp0s3 inet dhcp` + nl,
),
want: true,
want: true,
wantPatterns: []string{},
}, {
name: "return_patterns",
data: []byte(`source hello` + nl +
`source world` + nl +
`#iface enp0s3 inet static` + nl,
),
want: false,
wantPatterns: []string{"hello", "world"},
}, {
// This one tests if the first found valid interface prevents
// checking files under the `source` directive.
name: "ignore_patterns",
data: []byte(`source hello` + nl +
`source world` + nl +
`iface enp0s3 inet static` + nl,
),
want: true,
wantPatterns: []string{},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data)
has, err := ifacesStaticConfig(r, "enp0s3")
patterns, has, err := ifacesStaticConfig(r, "enp0s3")
require.NoError(t, err)
assert.Equal(t, tc.want, has)
assert.ElementsMatch(t, tc.wantPatterns, patterns)
})
}
}

View File

@@ -6,8 +6,14 @@ package aghnet
import (
"fmt"
"runtime"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
)
func canBindPrivilegedPorts() (can bool, err error) {
return aghos.HaveAdminRights()
}
func ifaceHasStaticIP(string) (bool, error) {
return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS)
}

1
internal/aghnet/testdata/ifaces vendored Normal file
View File

@@ -0,0 +1 @@
iface sample_name inet static

View File

@@ -0,0 +1,5 @@
# The "testdata" part is added here because the test is actually run from the
# parent directory. Real interface files usually contain only absolute paths.
source ./testdata/ifaces
source ./testdata/*

View File

@@ -1,4 +1,6 @@
// Package aghos contains utilities for functions requiring system calls.
// Package aghos contains utilities for functions requiring system calls and
// other OS-specific APIs. OS-specific network handling should go to aghnet
// instead.
package aghos
import (
@@ -29,12 +31,6 @@ func Unsupported(op string) (err error) {
}
}
// CanBindPrivilegedPorts checks if current process can bind to privileged
// ports.
func CanBindPrivilegedPorts() (can bool, err error) {
return canBindPrivilegedPorts()
}
// SetRlimit sets user-specified limit of how many fd's we can use.
//
// See https://github.com/AdguardTeam/AdGuardHome/internal/issues/659.

View File

@@ -8,10 +8,6 @@ import (
"syscall"
)
func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights()
}
func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit
rlim.Max = val

View File

@@ -8,10 +8,6 @@ import (
"syscall"
)
func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights()
}
func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit
rlim.Max = int64(val)

View File

@@ -9,18 +9,8 @@ import (
"path/filepath"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
func canBindPrivilegedPorts() (can bool, err error) {
cnbs, err := unix.PrctlRetInt(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, unix.CAP_NET_BIND_SERVICE, 0, 0)
// Don't check the error because it's always nil on Linux.
adm, _ := haveAdminRights()
return cnbs == 1 || adm, err
}
func setRlimit(val uint64) (err error) {
var rlim syscall.Rlimit
rlim.Max = val

View File

@@ -9,10 +9,6 @@ import (
"golang.org/x/sys/windows"
)
func canBindPrivilegedPorts() (can bool, err error) {
return HaveAdminRights()
}
func setRlimit(val uint64) (err error) {
return Unsupported("setrlimit")
}

View File

@@ -4,6 +4,7 @@ package dhcpd
import (
"encoding/json"
"fmt"
"net"
"os"
"time"
@@ -31,7 +32,7 @@ func normalizeIP(ip net.IP) net.IP {
}
// Load lease table from DB
func (s *Server) dbLoad() {
func (s *Server) dbLoad() (err error) {
dynLeases := []*Lease{}
staticLeases := []*Lease{}
v6StaticLeases := []*Lease{}
@@ -40,17 +41,16 @@ func (s *Server) dbLoad() {
data, err := os.ReadFile(s.conf.DBFilePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Error("dhcp: can't read file %q: %v", s.conf.DBFilePath, err)
return fmt.Errorf("reading db: %w", err)
}
return
return nil
}
obj := []leaseJSON{}
err = json.Unmarshal(data, &obj)
if err != nil {
log.Error("dhcp: invalid DB: %v", err)
return
return fmt.Errorf("decoding db: %w", err)
}
numLeases := len(obj)
@@ -85,15 +85,23 @@ func (s *Server) dbLoad() {
}
leases4 := normalizeLeases(staticLeases, dynLeases)
s.srv4.ResetLeases(leases4)
err = s.srv4.ResetLeases(leases4)
if err != nil {
return fmt.Errorf("resetting dhcpv4 leases: %w", err)
}
leases6 := normalizeLeases(v6StaticLeases, v6DynLeases)
if s.srv6 != nil {
s.srv6.ResetLeases(leases6)
err = s.srv6.ResetLeases(leases6)
if err != nil {
return fmt.Errorf("resetting dhcpv6 leases: %w", err)
}
}
log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB",
len(leases4), len(leases6), numLeases)
return nil
}
// Skip duplicate leases
@@ -124,20 +132,24 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
}
// Store lease table in DB
func (s *Server) dbStore() {
var leases []leaseJSON
func (s *Server) dbStore() (err error) {
// Use an empty slice here as opposed to nil so that it doesn't write
// "null" into the database file if leases are empty.
leases := []leaseJSON{}
leases4 := s.srv4.getLeasesRef()
for _, l := range leases4 {
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: l.HWAddr,
IP: l.IP,
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}
@@ -147,29 +159,30 @@ func (s *Server) dbStore() {
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: l.HWAddr,
IP: l.IP,
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}
}
data, err := json.Marshal(leases)
var data []byte
data, err = json.Marshal(leases)
if err != nil {
log.Error("json.Marshal: %v", err)
return
return fmt.Errorf("encoding db: %w", err)
}
err = maybe.WriteFile(s.conf.DBFilePath, data, 0o644)
if err != nil {
log.Error("dhcp: can't store lease table on disk: %v filename: %s",
err, s.conf.DBFilePath)
return
return fmt.Errorf("writing db: %w", err)
}
log.Info("dhcp: stored %d leases in DB", len(leases))
log.Info("dhcp: stored %d leases in db", len(leases))
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"runtime"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/log"
)
@@ -36,6 +37,37 @@ type Lease struct {
IP net.IP `json:"ip"`
}
// Clone returns a deep copy of l.
func (l *Lease) Clone() (clone *Lease) {
if l == nil {
return nil
}
return &Lease{
Expiry: l.Expiry,
Hostname: l.Hostname,
HWAddr: aghnet.CloneMAC(l.HWAddr),
IP: aghnet.CloneIP(l.IP),
}
}
// IsBlocklisted returns true if the lease is blocklisted.
//
// TODO(a.garipov): Just make it a boolean field.
func (l *Lease) IsBlocklisted() (ok bool) {
if len(l.HWAddr) == 0 {
return false
}
for _, b := range l.HWAddr {
if b != 0 {
return false
}
}
return true
}
// IsStatic returns true if the lease is static.
//
// TODO(a.garipov): Just make it a boolean field.
@@ -131,16 +163,27 @@ type Server struct {
onLeaseChanged []OnLeaseChangedT
}
// GetLeasesFlags are the flags for GetLeases.
type GetLeasesFlags uint8
// GetLeasesFlags values
const (
LeasesDynamic GetLeasesFlags = 0b0001
LeasesStatic GetLeasesFlags = 0b0010
LeasesAll = LeasesDynamic | LeasesStatic
)
// ServerInterface is an interface for servers.
type ServerInterface interface {
Enabled() (ok bool)
Leases(flags int) []Lease
Leases(flags GetLeasesFlags) (leases []*Lease)
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
}
// Create - create object
func Create(conf ServerConfig) *Server {
s := &Server{}
func Create(conf ServerConfig) (s *Server, err error) {
s = &Server{}
s.conf.Enabled = conf.Enabled
s.conf.InterfaceName = conf.InterfaceName
@@ -166,15 +209,18 @@ func Create(conf ServerConfig) *Server {
webHandlersRegistered = true
}
var err4, err6 error
v4conf := conf.Conf4
v4conf.Enabled = s.conf.Enabled
if len(v4conf.RangeStart) == 0 {
v4conf.Enabled = false
}
v4conf.InterfaceName = s.conf.InterfaceName
v4conf.notify = s.onNotify
s.srv4, err4 = v4Create(v4conf)
s.srv4, err = v4Create(v4conf)
if err != nil {
return nil, fmt.Errorf("creating dhcpv4 srv: %w", err)
}
v6conf := conf.Conf6
v6conf.Enabled = s.conf.Enabled
@@ -183,29 +229,26 @@ func Create(conf ServerConfig) *Server {
}
v6conf.InterfaceName = s.conf.InterfaceName
v6conf.notify = s.onNotify
s.srv6, err6 = v6Create(v6conf)
if err4 != nil {
log.Error("%s", err4)
return nil
}
if err6 != nil {
log.Error("%s", err6)
return nil
s.srv6, err = v6Create(v6conf)
if err != nil {
return nil, fmt.Errorf("creating dhcpv6 srv: %w", err)
}
s.conf.Conf4 = conf.Conf4
s.conf.Conf6 = conf.Conf6
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured")
return nil
return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured")
}
// we can't delay database loading until DHCP server is started,
// because we need static leases functionality available beforehand
s.dbLoad()
return s
// Don't delay database loading until the DHCP server is started,
// because we need static leases functionality available beforehand.
err = s.dbLoad()
if err != nil {
return nil, fmt.Errorf("loading db: %w", err)
}
return s, nil
}
// Enabled returns true when the server is enabled.
@@ -213,10 +256,30 @@ func (s *Server) Enabled() (ok bool) {
return s.conf.Enabled
}
// resetLeases resets all leases in the lease database.
func (s *Server) resetLeases() (err error) {
err = s.srv4.ResetLeases(nil)
if err != nil {
return err
}
if s.srv6 != nil {
err = s.srv6.ResetLeases(nil)
if err != nil {
return err
}
}
return s.dbStore()
}
// server calls this function after DB is updated
func (s *Server) onNotify(flags uint32) {
if flags == LeaseChangedDBStore {
s.dbStore()
err := s.dbStore()
if err != nil {
log.Error("updating db: %s", err)
}
return
}
@@ -263,21 +326,23 @@ func (s *Server) Start() (err error) {
}
// Stop closes the listening UDP socket
func (s *Server) Stop() {
s.srv4.Stop()
s.srv6.Stop()
}
func (s *Server) Stop() (err error) {
err = s.srv4.Stop()
if err != nil {
return err
}
// flags for Leases() function
const (
LeasesDynamic = 1
LeasesStatic = 2
LeasesAll = LeasesDynamic | LeasesStatic
)
err = s.srv6.Stop()
if err != nil {
return err
}
return nil
}
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
// concurrent use.
func (s *Server) Leases(flags int) (leases []Lease) {
func (s *Server) Leases(flags GetLeasesFlags) (leases []*Lease) {
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
}
@@ -290,6 +355,6 @@ func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
}
// AddStaticLease - add static v4 lease
func (s *Server) AddStaticLease(lease Lease) error {
return s.srv4.AddStaticLease(lease)
func (s *Server) AddStaticLease(l *Lease) error {
return s.srv4.AddStaticLease(l)
}

View File

@@ -43,7 +43,7 @@ func TestDB(t *testing.T) {
s.srv6, err = v6Create(V6ServerConf{})
require.NoError(t, err)
leases := []Lease{{
leases := []*Lease{{
Expiry: time.Now().Add(time.Hour),
Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
@@ -57,18 +57,24 @@ func TestDB(t *testing.T) {
srv4, ok := s.srv4.(*v4Server)
require.True(t, ok)
err = srv4.addLease(&leases[0])
err = srv4.addLease(leases[0])
require.NoError(t, err)
err = s.srv4.AddStaticLease(leases[1])
require.NoError(t, err)
s.dbStore()
err = s.dbStore()
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, os.Remove(dbFilename))
})
s.srv4.ResetLeases(nil)
s.dbLoad()
err = s.srv4.ResetLeases(nil)
require.NoError(t, err)
err = s.dbLoad()
require.NoError(t, err)
ll := s.srv4.GetLeases(LeasesAll)
require.Len(t, ll, len(leases))

View File

@@ -60,12 +60,12 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
// dhcpStatusResponse is the response for /control/dhcp/status endpoint.
type dhcpStatusResponse struct {
Enabled bool `json:"enabled"`
IfaceName string `json:"interface_name"`
V4 V4ServerConf `json:"v4"`
V6 V6ServerConf `json:"v6"`
Leases []Lease `json:"leases"`
StaticLeases []Lease `json:"static_leases"`
Leases []*Lease `json:"leases"`
StaticLeases []*Lease `json:"static_leases"`
Enabled bool `json:"enabled"`
}
func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
@@ -146,12 +146,68 @@ type dhcpServerConfigJSON struct {
Enabled nullBool `json:"enabled"`
}
func (s *Server) handleDHCPSetConfigV4(
conf *dhcpServerConfigJSON,
) (srv4 DHCPServer, enabled bool, err error) {
if conf.V4 == nil {
return nil, false, nil
}
v4Conf := v4JSONToServerConf(conf.V4)
v4Conf.Enabled = conf.Enabled == nbTrue
if len(v4Conf.RangeStart) == 0 {
v4Conf.Enabled = false
}
enabled = v4Conf.Enabled
v4Conf.InterfaceName = conf.InterfaceName
c4 := V4ServerConf{}
s.srv4.WriteDiskConfig4(&c4)
v4Conf.notify = c4.notify
v4Conf.ICMPTimeout = c4.ICMPTimeout
v4Conf.Options = c4.Options
srv4, err = v4Create(v4Conf)
return srv4, enabled, err
}
func (s *Server) handleDHCPSetConfigV6(
conf *dhcpServerConfigJSON,
) (srv6 DHCPServer, enabled bool, err error) {
if conf.V6 == nil {
return nil, false, nil
}
v6Conf := v6JSONToServerConf(conf.V6)
v6Conf.Enabled = conf.Enabled == nbTrue
if len(v6Conf.RangeStart) == 0 {
v6Conf.Enabled = false
}
// Don't overwrite the RA/SLAAC settings from the config file.
//
// TODO(a.garipov): Perhaps include them into the request to allow
// changing them from the HTTP API?
v6Conf.RASLAACOnly = s.conf.Conf6.RASLAACOnly
v6Conf.RAAllowSLAAC = s.conf.Conf6.RAAllowSLAAC
enabled = v6Conf.Enabled
v6Conf.InterfaceName = conf.InterfaceName
v6Conf.notify = s.onNotify
srv6, err = v6Create(v6Conf)
return srv6, enabled, err
}
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
conf := dhcpServerConfigJSON{}
conf := &dhcpServerConfigJSON{}
conf.Enabled = boolToNullBool(s.conf.Enabled)
conf.InterfaceName = s.conf.InterfaceName
err := json.NewDecoder(r.Body).Decode(&conf)
err := json.NewDecoder(r.Body).Decode(conf)
if err != nil {
httpError(r, w, http.StatusBadRequest,
"failed to parse new dhcp config json: %s", err)
@@ -159,61 +215,18 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
var s4 DHCPServer
var s6 DHCPServer
v4Enabled := false
v6Enabled := false
srv4, v4Enabled, err := s.handleDHCPSetConfigV4(conf)
if err != nil {
httpError(r, w, http.StatusBadRequest, "bad dhcpv4 configuration: %s", err)
if conf.V4 != nil {
v4Conf := v4JSONToServerConf(conf.V4)
v4Conf.Enabled = conf.Enabled == nbTrue
if len(v4Conf.RangeStart) == 0 {
v4Conf.Enabled = false
}
v4Enabled = v4Conf.Enabled
v4Conf.InterfaceName = conf.InterfaceName
c4 := V4ServerConf{}
s.srv4.WriteDiskConfig4(&c4)
v4Conf.notify = c4.notify
v4Conf.ICMPTimeout = c4.ICMPTimeout
v4Conf.Options = c4.Options
s4, err = v4Create(v4Conf)
if err != nil {
httpError(r, w, http.StatusBadRequest,
"invalid dhcpv4 configuration: %s", err)
return
}
return
}
if conf.V6 != nil {
v6Conf := v6JSONToServerConf(conf.V6)
v6Conf.Enabled = conf.Enabled == nbTrue
if len(v6Conf.RangeStart) == 0 {
v6Conf.Enabled = false
}
srv6, v6Enabled, err := s.handleDHCPSetConfigV6(conf)
if err != nil {
httpError(r, w, http.StatusBadRequest, "bad dhcpv6 configuration: %s", err)
// Don't overwrite the RA/SLAAC settings from the config file.
//
// TODO(a.garipov): Perhaps include them into the request to
// allow changing them from the HTTP API?
v6Conf.RASLAACOnly = s.conf.Conf6.RASLAACOnly
v6Conf.RAAllowSLAAC = s.conf.Conf6.RAAllowSLAAC
v6Enabled = v6Conf.Enabled
v6Conf.InterfaceName = conf.InterfaceName
v6Conf.notify = s.onNotify
s6, err = v6Create(v6Conf)
if err != nil {
httpError(r, w, http.StatusBadRequest,
"invalid dhcpv6 configuration: %s", err)
return
}
return
}
if conf.Enabled == nbTrue && !v4Enabled && !v6Enabled {
@@ -223,7 +236,12 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
s.Stop()
err = s.Stop()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "stopping dhcp: %s", err)
return
}
if conf.Enabled != nbNull {
s.conf.Enabled = conf.Enabled == nbTrue
@@ -233,16 +251,22 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
s.conf.InterfaceName = conf.InterfaceName
}
if s4 != nil {
s.srv4 = s4
if srv4 != nil {
s.srv4 = srv4
}
if s6 != nil {
s.srv6 = s6
if srv6 != nil {
s.srv6 = srv6
}
s.conf.ConfigModified()
s.dbLoad()
err = s.dbLoad()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "loading leases db: %s", err)
return
}
if s.conf.Enabled {
var code int
@@ -431,26 +455,26 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
}
func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
lj := Lease{}
err := json.NewDecoder(r.Body).Decode(&lj)
l := &Lease{}
err := json.NewDecoder(r.Body).Decode(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
if lj.IP == nil {
if l.IP == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP")
return
}
ip4 := lj.IP.To4()
ip4 := l.IP.To4()
if ip4 == nil {
lj.IP = lj.IP.To16()
l.IP = l.IP.To16()
err = s.srv6.AddStaticLease(lj)
err = s.srv6.AddStaticLease(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
}
@@ -458,8 +482,8 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
return
}
lj.IP = ip4
err = s.srv4.AddStaticLease(lj)
l.IP = ip4
err = s.srv4.AddStaticLease(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
@@ -468,26 +492,26 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
}
func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
lj := Lease{}
err := json.NewDecoder(r.Body).Decode(&lj)
l := &Lease{}
err := json.NewDecoder(r.Body).Decode(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
if lj.IP == nil {
if l.IP == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP")
return
}
ip4 := lj.IP.To4()
ip4 := l.IP.To4()
if ip4 == nil {
lj.IP = lj.IP.To16()
l.IP = l.IP.To16()
err = s.srv6.RemoveStaticLease(lj)
err = s.srv6.RemoveStaticLease(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
}
@@ -495,8 +519,8 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
return
}
lj.IP = ip4
err = s.srv4.RemoveStaticLease(lj)
l.IP = ip4
err = s.srv4.RemoveStaticLease(l)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
@@ -505,11 +529,16 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
}
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
s.Stop()
err := s.Stop()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "stopping dhcp: %s", err)
err := os.Remove(s.conf.DBFilePath)
return
}
err = os.Remove(s.conf.DBFilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Error("dhcp: removing %q: %s", s.conf.DBFilePath, err)
log.Error("dhcp: removing db: %s", err)
}
oldconf := s.conf
@@ -531,6 +560,16 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
s.conf.ConfigModified()
}
func (s *Server) handleResetLeases(w http.ResponseWriter, r *http.Request) {
err := s.resetLeases()
if err != nil {
msg := "resetting leases: %s"
httpError(r, w, http.StatusInternalServerError, msg, err)
return
}
}
func (s *Server) registerHandlers() {
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/status", s.handleDHCPStatus)
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/interfaces", s.handleDHCPInterfaces)
@@ -539,6 +578,7 @@ func (s *Server) registerHandlers() {
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/add_static_lease", s.handleDHCPAddStaticLease)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/remove_static_lease", s.handleDHCPRemoveStaticLease)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset", s.handleReset)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset_leases", s.handleResetLeases)
}
// jsonError is a generic JSON error response.
@@ -579,4 +619,5 @@ func (s *Server) registerNotImplementedHandlers() {
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/add_static_lease", h)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/remove_static_lease", h)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset", h)
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset_leases", h)
}

View File

@@ -7,14 +7,14 @@ import (
// DHCPServer - DHCP server interface
type DHCPServer interface {
// ResetLeases - reset leases
ResetLeases(leases []*Lease)
// GetLeases - get leases
GetLeases(flags int) []Lease
// ResetLeases resets leases.
ResetLeases(leases []*Lease) (err error)
// GetLeases returns deep clones of the current leases.
GetLeases(flags GetLeasesFlags) (leases []*Lease)
// AddStaticLease - add a static lease
AddStaticLease(lease Lease) error
AddStaticLease(l *Lease) (err error)
// RemoveStaticLease - remove a static lease
RemoveStaticLease(l Lease) error
RemoveStaticLease(l *Lease) (err error)
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
FindMACbyIP(ip net.IP) net.HardwareAddr
@@ -24,9 +24,9 @@ type DHCPServer interface {
WriteDiskConfig6(c *V6ServerConf)
// Start - start server
Start() error
Start() (err error)
// Stop - stop server
Stop()
Stop() (err error)
getLeasesRef() []*Lease
}

View File

@@ -96,9 +96,9 @@ func (s *v4Server) validHostnameForClient(cliHostname string, ip net.IP) (hostna
return hostname
}
// ResetLeases - reset leases
func (s *v4Server) ResetLeases(leases []*Lease) {
var err error
// ResetLeases resets leases.
func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
if !s.conf.Enabled {
return
@@ -125,6 +125,8 @@ func (s *v4Server) ResetLeases(leases []*Lease) {
continue
}
}
return nil
}
// getLeasesRef returns the actual leases slice. For internal use only.
@@ -154,14 +156,12 @@ func (s *v4Server) isBlocklisted(l *Lease) (ok bool) {
// GetLeases returns the list of current DHCP leases. It is safe for concurrent
// use.
func (s *v4Server) GetLeases(flags int) (res []Lease) {
func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
// The function shouldn't return nil, because zero-length slice behaves
// differently in cases like marshalling. Our front-end also requires
// a non-nil value in the response.
res = []Lease{}
leases = []*Lease{}
// TODO(a.garipov): Remove the silly bit twiddling and make GetLeases
// accept booleans. Seriously, this doesn't even save stack space.
getDynamic := flags&LeasesDynamic != 0
getStatic := flags&LeasesStatic != 0
@@ -171,17 +171,17 @@ func (s *v4Server) GetLeases(flags int) (res []Lease) {
now := time.Now()
for _, l := range s.leases {
if getDynamic && l.Expiry.After(now) && !s.isBlocklisted(l) {
res = append(res, *l)
leases = append(leases, l.Clone())
continue
}
if getStatic && l.IsStatic() {
res = append(res, *l)
leases = append(leases, l.Clone())
}
}
return res
return leases
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
@@ -305,7 +305,7 @@ func (s *v4Server) addLease(l *Lease) (err error) {
}
// rmLease removes a lease with the same properties.
func (s *v4Server) rmLease(lease Lease) (err error) {
func (s *v4Server) rmLease(lease *Lease) (err error) {
if len(s.leases) == 0 {
return nil
}
@@ -326,7 +326,7 @@ func (s *v4Server) rmLease(lease Lease) (err error) {
}
// AddStaticLease adds a static lease. It is safe for concurrent use.
func (s *v4Server) AddStaticLease(l Lease) (err error) {
func (s *v4Server) AddStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: adding static lease: %w") }()
if ip4 := l.IP.To4(); ip4 == nil {
@@ -365,7 +365,7 @@ func (s *v4Server) AddStaticLease(l Lease) (err error) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
err = s.rmDynamicLease(&l)
err = s.rmDynamicLease(l)
if err != nil {
err = fmt.Errorf(
"removing dynamic leases for %s (%s): %w",
@@ -377,7 +377,7 @@ func (s *v4Server) AddStaticLease(l Lease) (err error) {
return
}
err = s.addLease(&l)
err = s.addLease(l)
if err != nil {
err = fmt.Errorf("adding static lease for %s (%s): %w", l.IP, l.HWAddr, err)
@@ -395,7 +395,7 @@ func (s *v4Server) AddStaticLease(l Lease) (err error) {
}
// RemoveStaticLease removes a static lease. It is safe for concurrent use.
func (s *v4Server) RemoveStaticLease(l Lease) (err error) {
func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
if len(l.IP) != 4 {
@@ -993,15 +993,15 @@ func (s *v4Server) Start() (err error) {
}
// Stop - stop server
func (s *v4Server) Stop() {
func (s *v4Server) Stop() (err error) {
if s.srv == nil {
return
}
log.Debug("dhcpv4: stopping")
err := s.srv.Close()
err = s.srv.Close()
if err != nil {
log.Error("dhcpv4: srv.Close: %s", err)
return fmt.Errorf("closing dhcpv4 srv: %w", err)
}
// Signal to the clients containers in packages home and dnsforward that
@@ -1009,6 +1009,8 @@ func (s *v4Server) Stop() {
s.conf.notify(LeaseChangedRemovedAll)
s.srv = nil
return nil
}
// Create DHCPv4 server

View File

@@ -9,16 +9,15 @@ import "net"
type winServer struct{}
func (s *winServer) ResetLeases(leases []*Lease) {}
func (s *winServer) GetLeases(flags int) []Lease { return nil }
func (s *winServer) getLeasesRef() []*Lease { return nil }
func (s *winServer) AddStaticLease(lease Lease) error { return nil }
func (s *winServer) RemoveStaticLease(l Lease) error { return nil }
func (s *winServer) FindMACbyIP(ip net.IP) net.HardwareAddr { return nil }
func (s *winServer) WriteDiskConfig4(c *V4ServerConf) {}
func (s *winServer) WriteDiskConfig6(c *V6ServerConf) {}
func (s *winServer) Start() error { return nil }
func (s *winServer) Stop() {}
func (s *winServer) Reset() {}
func v4Create(conf V4ServerConf) (DHCPServer, error) { return &winServer{}, nil }
func v6Create(conf V6ServerConf) (DHCPServer, error) { return &winServer{}, nil }
func (s *winServer) ResetLeases(_ []*Lease) (err error) { return nil }
func (s *winServer) GetLeases(_ GetLeasesFlags) (leases []*Lease) { return nil }
func (s *winServer) getLeasesRef() []*Lease { return nil }
func (s *winServer) AddStaticLease(_ *Lease) (err error) { return nil }
func (s *winServer) RemoveStaticLease(_ *Lease) (err error) { return nil }
func (s *winServer) FindMACbyIP(ip net.IP) (mac net.HardwareAddr) { return nil }
func (s *winServer) WriteDiskConfig4(c *V4ServerConf) {}
func (s *winServer) WriteDiskConfig6(c *V6ServerConf) {}
func (s *winServer) Start() (err error) { return nil }
func (s *winServer) Stop() (err error) { return nil }
func v4Create(conf V4ServerConf) (DHCPServer, error) { return &winServer{}, nil }
func v6Create(conf V6ServerConf) (DHCPServer, error) { return &winServer{}, nil }

View File

@@ -30,7 +30,7 @@ func TestV4_AddRemove_static(t *testing.T) {
assert.Empty(t, ls)
// Add static lease.
l := Lease{
l := &Lease{
Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150},
@@ -50,7 +50,7 @@ func TestV4_AddRemove_static(t *testing.T) {
assert.True(t, ls[0].IsStatic())
// Try to remove static lease.
err = s.RemoveStaticLease(Lease{
err = s.RemoveStaticLease(&Lease{
IP: net.IP{192, 168, 10, 110},
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
})
@@ -92,7 +92,7 @@ func TestV4_AddReplace(t *testing.T) {
require.NoError(t, err)
}
stLeases := []Lease{{
stLeases := []*Lease{{
Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150},
@@ -134,7 +134,7 @@ func TestV4StaticLease_Get(t *testing.T) {
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
l := Lease{
l := &Lease{
Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150},

View File

@@ -57,10 +57,12 @@ func ip6InRange(start, ip net.IP) bool {
return start[15] <= ip[15]
}
// ResetLeases - reset leases
func (s *v6Server) ResetLeases(ll []*Lease) {
// ResetLeases resets leases.
func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
s.leases = nil
for _, l := range ll {
for _, l := range leases {
if l.Expiry.Unix() != leaseExpireStatic &&
!ip6InRange(s.conf.ipStart, l.IP) {
@@ -72,28 +74,31 @@ func (s *v6Server) ResetLeases(ll []*Lease) {
s.addLease(l)
}
return nil
}
// GetLeases - get current leases
func (s *v6Server) GetLeases(flags int) []Lease {
// GetLeases returns the list of current DHCP leases. It is safe for concurrent
// use.
func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
// The function shouldn't return nil value because zero-length slice
// behaves differently in cases like marshalling. Our front-end also
// requires non-nil value in the response.
result := []Lease{}
leases = []*Lease{}
s.leasesLock.Lock()
for _, lease := range s.leases {
if lease.Expiry.Unix() == leaseExpireStatic {
for _, l := range s.leases {
if l.Expiry.Unix() == leaseExpireStatic {
if (flags & LeasesStatic) != 0 {
result = append(result, *lease)
leases = append(leases, l.Clone())
}
} else {
if (flags & LeasesDynamic) != 0 {
result = append(result, *lease)
leases = append(leases, l.Clone())
}
}
}
s.leasesLock.Unlock()
return result
return leases
}
// getLeasesRef returns the actual leases slice. For internal use only.
@@ -133,12 +138,11 @@ func (s *v6Server) leaseRemoveSwapByIndex(i int) {
// Remove a dynamic lease with the same properties
// Return error if a static lease is found
func (s *v6Server) rmDynamicLease(lease Lease) error {
func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
for i := 0; i < len(s.leases); i++ {
l := s.leases[i]
if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
@@ -147,11 +151,11 @@ func (s *v6Server) rmDynamicLease(lease Lease) error {
if i == len(s.leases) {
break
}
l = s.leases[i]
}
if net.IP.Equal(l.IP, lease.IP) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
@@ -159,11 +163,12 @@ func (s *v6Server) rmDynamicLease(lease Lease) error {
s.leaseRemoveSwapByIndex(i)
}
}
return nil
}
// AddStaticLease adds a static lease. It is safe for concurrent use.
func (s *v6Server) AddStaticLease(l Lease) (err error) {
func (s *v6Server) AddStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
if len(l.IP) != 16 {
@@ -181,18 +186,21 @@ func (s *v6Server) AddStaticLease(l Lease) (err error) {
err = s.rmDynamicLease(l)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.addLease(&l)
s.addLease(l)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAddedStatic)
return nil
}
// RemoveStaticLease removes a static lease. It is safe for concurrent use.
func (s *v6Server) RemoveStaticLease(l Lease) (err error) {
func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
if len(l.IP) != 16 {
@@ -224,19 +232,20 @@ func (s *v6Server) addLease(l *Lease) {
}
// Remove a lease with the same properties
func (s *v6Server) rmLease(lease Lease) error {
func (s *v6Server) rmLease(lease *Lease) (err error) {
for i, l := range s.leases {
if net.IP.Equal(l.IP, lease.IP) {
if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
l.Hostname != lease.Hostname {
return fmt.Errorf("lease not found")
}
s.leaseRemoveSwapByIndex(i)
return nil
}
}
return fmt.Errorf("lease not found")
}
@@ -654,10 +663,10 @@ func (s *v6Server) Start() (err error) {
}
// Stop - stop server
func (s *v6Server) Stop() {
err := s.ra.Close()
func (s *v6Server) Stop() (err error) {
err = s.ra.Close()
if err != nil {
log.Error("dhcpv6: s.ra.Close: %s", err)
return fmt.Errorf("closing ra ctx: %w", err)
}
// DHCPv6 server may not be initialized if ra_slaac_only=true
@@ -668,11 +677,13 @@ func (s *v6Server) Stop() {
log.Debug("dhcpv6: stopping")
err = s.srv.Close()
if err != nil {
log.Error("dhcpv6: srv.Close: %s", err)
return fmt.Errorf("closing dhcpv6 srv: %w", err)
}
// now server.Serve() will return
s.srv = nil
return nil
}
// Create DHCPv6 server

View File

@@ -22,34 +22,39 @@ func TestV6_AddRemove_static(t *testing.T) {
RangeStart: net.ParseIP("2001::1"),
notify: notify6,
})
require.Nil(t, err)
require.NoError(t, err)
require.Empty(t, s.GetLeases(LeasesStatic))
// Add static lease.
l := Lease{
l := &Lease{
IP: net.ParseIP("2001::1"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}
require.Nil(t, s.AddStaticLease(l))
err = s.AddStaticLease(l)
require.NoError(t, err)
// Try to add the same static lease.
require.NotNil(t, s.AddStaticLease(l))
err = s.AddStaticLease(l)
require.Error(t, err)
ls := s.GetLeases(LeasesStatic)
require.Len(t, ls, 1)
assert.Equal(t, l.IP, ls[0].IP)
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
// Try to remove non-existent static lease.
require.NotNil(t, s.RemoveStaticLease(Lease{
err = s.RemoveStaticLease(&Lease{
IP: net.ParseIP("2001::2"),
HWAddr: l.HWAddr,
}))
})
require.Error(t, err)
// Remove static lease.
require.Nil(t, s.RemoveStaticLease(l))
err = s.RemoveStaticLease(l)
require.NoError(t, err)
assert.Empty(t, s.GetLeases(LeasesStatic))
}
@@ -60,7 +65,8 @@ func TestV6_AddReplace(t *testing.T) {
RangeStart: net.ParseIP("2001::1"),
notify: notify6,
})
require.Nil(t, err)
require.NoError(t, err)
s, ok := sIface.(*v6Server)
require.True(t, ok)
@@ -77,7 +83,7 @@ func TestV6_AddReplace(t *testing.T) {
s.addLease(l)
}
stLeases := []Lease{{
stLeases := []*Lease{{
IP: net.ParseIP("2001::1"),
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}, {
@@ -86,7 +92,8 @@ func TestV6_AddReplace(t *testing.T) {
}}
for _, l := range stLeases {
require.Nil(t, s.AddStaticLease(l))
err = s.AddStaticLease(l)
require.NoError(t, err)
}
ls := s.GetLeases(LeasesStatic)
@@ -106,8 +113,9 @@ func TestV6GetLease(t *testing.T) {
RangeStart: net.ParseIP("2001::1"),
notify: notify6,
})
require.Nil(t, err)
require.NoError(t, err)
s, ok := sIface.(*v6Server)
require.True(t, ok)
dnsAddr := net.ParseIP("2000::1")
@@ -118,33 +126,36 @@ func TestV6GetLease(t *testing.T) {
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}
l := Lease{
l := &Lease{
IP: net.ParseIP("2001::1"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}
require.Nil(t, s.AddStaticLease(l))
err = s.AddStaticLease(l)
require.NoError(t, err)
var req, resp, msg *dhcpv6.Message
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
t.Run("solicit", func(t *testing.T) {
req, err = dhcpv6.NewSolicit(mac)
require.Nil(t, err)
require.NoError(t, err)
msg, err = req.GetInnerMessage()
require.Nil(t, err)
require.NoError(t, err)
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, s.process(msg, req, resp))
})
require.Nil(t, err)
require.NoError(t, err)
resp.AddOption(dhcpv6.OptServerID(s.sid))
var oia *dhcpv6.OptIANA
var oiaAddr *dhcpv6.OptIAAddress
t.Run("advertise", func(t *testing.T) {
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
@@ -154,20 +165,21 @@ func TestV6GetLease(t *testing.T) {
t.Run("request", func(t *testing.T) {
req, err = dhcpv6.NewRequestFromAdvertise(resp)
require.Nil(t, err)
require.NoError(t, err)
msg, err = req.GetInnerMessage()
require.Nil(t, err)
require.NoError(t, err)
resp, err = dhcpv6.NewReplyFromMessage(msg)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, s.process(msg, req, resp))
})
require.Nil(t, err)
require.NoError(t, err)
t.Run("reply", func(t *testing.T) {
require.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
@@ -182,6 +194,7 @@ func TestV6GetLease(t *testing.T) {
t.Run("lease", func(t *testing.T) {
ls := s.GetLeases(LeasesStatic)
require.Len(t, ls, 1)
assert.Equal(t, l.IP, ls[0].IP)
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
})
@@ -193,7 +206,8 @@ func TestV6GetDynamicLease(t *testing.T) {
RangeStart: net.ParseIP("2001::2"),
notify: notify6,
})
require.Nil(t, err)
require.NoError(t, err)
s, ok := sIface.(*v6Server)
require.True(t, ok)
@@ -209,23 +223,25 @@ func TestV6GetDynamicLease(t *testing.T) {
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
t.Run("solicit", func(t *testing.T) {
req, err = dhcpv6.NewSolicit(mac)
require.Nil(t, err)
require.NoError(t, err)
msg, err = req.GetInnerMessage()
require.Nil(t, err)
require.NoError(t, err)
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, s.process(msg, req, resp))
})
require.Nil(t, err)
require.NoError(t, err)
resp.AddOption(dhcpv6.OptServerID(s.sid))
var oia *dhcpv6.OptIANA
var oiaAddr *dhcpv6.OptIAAddress
t.Run("advertise", func(t *testing.T) {
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
@@ -233,20 +249,21 @@ func TestV6GetDynamicLease(t *testing.T) {
t.Run("request", func(t *testing.T) {
req, err = dhcpv6.NewRequestFromAdvertise(resp)
require.Nil(t, err)
require.NoError(t, err)
msg, err = req.GetInnerMessage()
require.Nil(t, err)
require.NoError(t, err)
resp, err = dhcpv6.NewReplyFromMessage(msg)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, s.process(msg, req, resp))
})
require.Nil(t, err)
require.NoError(t, err)
t.Run("reply", func(t *testing.T) {
require.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
@@ -254,11 +271,13 @@ func TestV6GetDynamicLease(t *testing.T) {
dnsAddrs := resp.Options.DNS()
require.Len(t, dnsAddrs, 1)
assert.Equal(t, dnsAddr, dnsAddrs[0])
t.Run("lease", func(t *testing.T) {
ls := s.GetLeases(LeasesDynamic)
require.Len(t, ls, 1)
assert.Equal(t, "2001::2", ls[0].IP.String())
assert.Equal(t, mac, ls[0].HWAddr)
})

View File

@@ -6,138 +6,163 @@ import (
"net"
"net/http"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
)
// accessCtx controls IP and client blocking that takes place before all other
// processing. An accessCtx is safe for concurrent use.
type accessCtx struct {
lock sync.Mutex
allowedIPs *aghnet.IPMap
blockedIPs *aghnet.IPMap
// allowedClients are the IP addresses of clients in the allowlist.
allowedClients *aghstrings.Set
allowedClientIDs *aghstrings.Set
blockedClientIDs *aghstrings.Set
// disallowedClients are the IP addresses of clients in the blocklist.
disallowedClients *aghstrings.Set
blockedHostsEng *urlfilter.DNSEngine
allowedClientsIPNet []net.IPNet // CIDRs of whitelist clients
disallowedClientsIPNet []net.IPNet // CIDRs of clients that should be blocked
blockedHostsEngine *urlfilter.DNSEngine // finds hosts that should be blocked
// TODO(a.garipov): Create a type for a set of IP networks.
// aghnet.IPNetSet?
allowedNets []*net.IPNet
blockedNets []*net.IPNet
}
func newAccessCtx(allowedClients, disallowedClients, blockedHosts []string) (a *accessCtx, err error) {
a = &accessCtx{
allowedClients: aghstrings.NewSet(),
disallowedClients: aghstrings.NewSet(),
}
// unit is a convenient alias for struct{}
type unit = struct{}
err = processIPCIDRArray(a.allowedClients, &a.allowedClientsIPNet, allowedClients)
if err != nil {
return nil, fmt.Errorf("processing allowed clients: %w", err)
}
// processAccessClients is a helper for processing a list of client strings,
// which may be an IP address, a CIDR, or a ClientID.
func processAccessClients(
clientStrs []string,
ips *aghnet.IPMap,
nets *[]*net.IPNet,
clientIDs *aghstrings.Set,
) (err error) {
for i, s := range clientStrs {
if ip := net.ParseIP(s); ip != nil {
ips.Set(ip, unit{})
} else if cidrIP, ipnet, cidrErr := net.ParseCIDR(s); cidrErr == nil {
ipnet.IP = cidrIP
*nets = append(*nets, ipnet)
} else {
idErr := ValidateClientID(s)
if idErr != nil {
return fmt.Errorf(
"value %q at index %d: bad ip, cidr, or clientid",
s,
i,
)
}
err = processIPCIDRArray(a.disallowedClients, &a.disallowedClientsIPNet, disallowedClients)
if err != nil {
return nil, fmt.Errorf("processing disallowed clients: %w", err)
}
b := &strings.Builder{}
for _, s := range blockedHosts {
aghstrings.WriteToBuilder(b, strings.ToLower(s), "\n")
}
listArray := []filterlist.RuleList{}
list := &filterlist.StringRuleList{
ID: int(0),
RulesText: b.String(),
IgnoreCosmetic: true,
}
listArray = append(listArray, list)
rulesStorage, err := filterlist.NewRuleStorage(listArray)
if err != nil {
return nil, fmt.Errorf("filterlist.NewRuleStorage(): %w", err)
}
a.blockedHostsEngine = urlfilter.NewDNSEngine(rulesStorage)
return a, nil
}
// Split array of IP or CIDR into 2 containers for fast search
func processIPCIDRArray(dst *aghstrings.Set, dstIPNet *[]net.IPNet, src []string) error {
for _, s := range src {
ip := net.ParseIP(s)
if ip != nil {
dst.Add(s)
continue
clientIDs.Add(s)
}
_, 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
// Returns the item from the "disallowedClients" list that lead to blocking IP.
// If it returns TRUE and an empty string, it means that the "allowedClients" is not empty,
// but the ip does not belong to it.
func (a *accessCtx) IsBlockedIP(ip net.IP) (bool, string) {
ipStr := ip.String()
// newAccessCtx creates a new accessCtx.
func newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessCtx, err error) {
a = &accessCtx{
allowedIPs: aghnet.NewIPMap(0),
blockedIPs: aghnet.NewIPMap(0),
a.lock.Lock()
defer a.lock.Unlock()
if a.allowedClients.Len() != 0 || len(a.allowedClientsIPNet) != 0 {
if a.allowedClients.Has(ipStr) {
return false, ""
}
if len(a.allowedClientsIPNet) != 0 {
for _, ipnet := range a.allowedClientsIPNet {
if ipnet.Contains(ip) {
return false, ""
}
}
}
return true, ""
allowedClientIDs: aghstrings.NewSet(),
blockedClientIDs: aghstrings.NewSet(),
}
if a.disallowedClients.Has(ipStr) {
return true, ipStr
err = processAccessClients(allowed, a.allowedIPs, &a.allowedNets, a.allowedClientIDs)
if err != nil {
return nil, fmt.Errorf("adding allowed: %w", err)
}
if len(a.disallowedClientsIPNet) != 0 {
for _, ipnet := range a.disallowedClientsIPNet {
if ipnet.Contains(ip) {
return true, ipnet.String()
}
}
err = processAccessClients(blocked, a.blockedIPs, &a.blockedNets, a.blockedClientIDs)
if err != nil {
return nil, fmt.Errorf("adding blocked: %w", err)
}
return false, ""
b := &strings.Builder{}
for _, h := range blockedHosts {
aghstrings.WriteToBuilder(b, strings.ToLower(h), "\n")
}
lists := []filterlist.RuleList{
&filterlist.StringRuleList{
ID: int(0),
RulesText: b.String(),
IgnoreCosmetic: true,
},
}
rulesStrg, err := filterlist.NewRuleStorage(lists)
if err != nil {
return nil, fmt.Errorf("adding blocked hosts: %w", err)
}
a.blockedHostsEng = urlfilter.NewDNSEngine(rulesStrg)
return a, nil
}
// IsBlockedDomain - return TRUE if this domain should be blocked
func (a *accessCtx) IsBlockedDomain(host string) (ok bool) {
a.lock.Lock()
defer a.lock.Unlock()
// allowlistMode returns true if this *accessCtx is in the allowlist mode.
func (a *accessCtx) allowlistMode() (ok bool) {
return a.allowedIPs.Len() != 0 || a.allowedClientIDs.Len() != 0 || len(a.allowedNets) != 0
}
_, ok = a.blockedHostsEngine.Match(strings.ToLower(host))
// isBlockedClientID returns true if the ClientID should be blocked.
func (a *accessCtx) isBlockedClientID(id string) (ok bool) {
allowlistMode := a.allowlistMode()
if id == "" {
// In allowlist mode, consider requests without client IDs
// blocked by default.
return allowlistMode
}
if allowlistMode {
return !a.allowedClientIDs.Has(id)
}
return a.blockedClientIDs.Has(id)
}
// isBlockedHost returns true if host should be blocked.
func (a *accessCtx) isBlockedHost(host string) (ok bool) {
_, ok = a.blockedHostsEng.Match(strings.ToLower(host))
return ok
}
// isBlockedIP returns the status of the IP address blocking as well as the rule
// that blocked it.
func (a *accessCtx) isBlockedIP(ip net.IP) (blocked bool, rule string) {
blocked = true
ips := a.blockedIPs
ipnets := a.blockedNets
if a.allowlistMode() {
// Enable allowlist mode and use the allowlist sets.
blocked = false
ips = a.allowedIPs
ipnets = a.allowedNets
}
if _, ok := ips.Get(ip); ok {
return blocked, ip.String()
}
for _, ipnet := range ipnets {
if ipnet.Contains(ip) {
return blocked, ipnet.String()
}
}
return !blocked, ""
}
type accessListJSON struct {
AllowedClients []string `json:"allowed_clients"`
DisallowedClients []string `json:"disallowed_clients"`
@@ -161,62 +186,43 @@ func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
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)
httpError(r, w, http.StatusInternalServerError, "encoding response: %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)
list := accessListJSON{}
err := json.NewDecoder(r.Body).Decode(&list)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
httpError(r, w, http.StatusBadRequest, "decoding request: %s", err)
err = checkIPCIDRArray(j.AllowedClients)
if err == nil {
err = checkIPCIDRArray(j.DisallowedClients)
}
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
var a *accessCtx
a, err = newAccessCtx(j.AllowedClients, j.DisallowedClients, j.BlockedHosts)
a, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts)
if err != nil {
httpError(r, w, http.StatusBadRequest, "creating access ctx: %s", err)
return
}
defer log.Debug("Access: updated lists: %d, %d, %d",
len(j.AllowedClients), len(j.DisallowedClients), len(j.BlockedHosts))
defer log.Debug(
"access: updated lists: %d, %d, %d",
len(list.AllowedClients),
len(list.DisallowedClients),
len(list.BlockedHosts),
)
defer s.conf.ConfigModified()
s.serverLock.Lock()
defer s.serverLock.Unlock()
s.conf.AllowedClients = j.AllowedClients
s.conf.DisallowedClients = j.DisallowedClients
s.conf.BlockedHosts = j.BlockedHosts
s.conf.AllowedClients = list.AllowedClients
s.conf.DisallowedClients = list.DisallowedClients
s.conf.BlockedHosts = list.BlockedHosts
s.access = a
}

View File

@@ -8,99 +8,23 @@ import (
"github.com/stretchr/testify/require"
)
func TestIsBlockedIP(t *testing.T) {
const (
ip int = iota
cidr
)
func TestIsBlockedClientID(t *testing.T) {
clientID := "client-1"
clients := []string{clientID}
rules := []string{
ip: "1.1.1.1",
cidr: "2.2.0.0/16",
}
a, err := newAccessCtx(clients, nil, nil)
require.NoError(t, err)
testCases := []struct {
name string
allowed bool
ip net.IP
wantDis bool
wantRule string
}{{
name: "allow_ip",
allowed: true,
ip: net.IPv4(1, 1, 1, 1),
wantDis: false,
wantRule: "",
}, {
name: "disallow_ip",
allowed: true,
ip: net.IPv4(1, 1, 1, 2),
wantDis: true,
wantRule: "",
}, {
name: "allow_cidr",
allowed: true,
ip: net.IPv4(2, 2, 1, 1),
wantDis: false,
wantRule: "",
}, {
name: "disallow_cidr",
allowed: true,
ip: net.IPv4(2, 3, 1, 1),
wantDis: true,
wantRule: "",
}, {
name: "allow_ip",
allowed: false,
ip: net.IPv4(1, 1, 1, 1),
wantDis: true,
wantRule: rules[ip],
}, {
name: "disallow_ip",
allowed: false,
ip: net.IPv4(1, 1, 1, 2),
wantDis: false,
wantRule: "",
}, {
name: "allow_cidr",
allowed: false,
ip: net.IPv4(2, 2, 1, 1),
wantDis: true,
wantRule: rules[cidr],
}, {
name: "disallow_cidr",
allowed: false,
ip: net.IPv4(2, 3, 1, 1),
wantDis: false,
wantRule: "",
}}
assert.False(t, a.isBlockedClientID(clientID))
for _, tc := range testCases {
prefix := "allowed_"
if !tc.allowed {
prefix = "disallowed_"
}
a, err = newAccessCtx(nil, clients, nil)
require.NoError(t, err)
t.Run(prefix+tc.name, func(t *testing.T) {
allowedRules := rules
var disallowedRules []string
if !tc.allowed {
allowedRules, disallowedRules = disallowedRules, allowedRules
}
aCtx, err := newAccessCtx(allowedRules, disallowedRules, nil)
require.NoError(t, err)
disallowed, rule := aCtx.IsBlockedIP(tc.ip)
assert.Equal(t, tc.wantDis, disallowed)
assert.Equal(t, tc.wantRule, rule)
})
}
assert.True(t, a.isBlockedClientID(clientID))
}
func TestIsBlockedDomain(t *testing.T) {
aCtx, err := newAccessCtx(nil, nil, []string{
func TestIsBlockedHost(t *testing.T) {
a, err := newAccessCtx(nil, nil, []string{
"host1",
"*.host.com",
"||host3.com^",
@@ -108,50 +32,106 @@ func TestIsBlockedDomain(t *testing.T) {
require.NoError(t, err)
testCases := []struct {
name string
domain string
want bool
name string
host string
want bool
}{{
name: "plain_match",
domain: "host1",
want: true,
name: "plain_match",
host: "host1",
want: true,
}, {
name: "plain_mismatch",
domain: "host2",
want: false,
name: "plain_mismatch",
host: "host2",
want: false,
}, {
name: "wildcard_type-1_match_short",
domain: "asdf.host.com",
want: true,
name: "subdomain_match_short",
host: "asdf.host.com",
want: true,
}, {
name: "wildcard_type-1_match_long",
domain: "qwer.asdf.host.com",
want: true,
name: "subdomain_match_long",
host: "qwer.asdf.host.com",
want: true,
}, {
name: "wildcard_type-1_mismatch_no-lead",
domain: "host.com",
want: false,
name: "subdomain_mismatch_no_lead",
host: "host.com",
want: false,
}, {
name: "wildcard_type-1_mismatch_bad-asterisk",
domain: "asdf.zhost.com",
want: false,
name: "subdomain_mismatch_bad_asterisk",
host: "asdf.zhost.com",
want: false,
}, {
name: "wildcard_type-2_match_simple",
domain: "host3.com",
want: true,
name: "rule_match_simple",
host: "host3.com",
want: true,
}, {
name: "wildcard_type-2_match_complex",
domain: "asdf.host3.com",
want: true,
name: "rule_match_complex",
host: "asdf.host3.com",
want: true,
}, {
name: "wildcard_type-2_mismatch",
domain: ".host3.com",
want: false,
name: "rule_mismatch",
host: ".host3.com",
want: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, aCtx.IsBlockedDomain(tc.domain))
assert.Equal(t, tc.want, a.isBlockedHost(tc.host))
})
}
}
func TestIsBlockedIP(t *testing.T) {
clients := []string{
"1.2.3.4",
"5.6.7.8/24",
}
allowCtx, err := newAccessCtx(clients, nil, nil)
require.NoError(t, err)
blockCtx, err := newAccessCtx(nil, clients, nil)
require.NoError(t, err)
testCases := []struct {
name string
wantRule string
ip net.IP
wantBlocked bool
}{{
name: "match_ip",
wantRule: "1.2.3.4",
ip: net.IP{1, 2, 3, 4},
wantBlocked: true,
}, {
name: "match_cidr",
wantRule: "5.6.7.8/24",
ip: net.IP{5, 6, 7, 100},
wantBlocked: true,
}, {
name: "no_match_ip",
wantRule: "",
ip: net.IP{9, 2, 3, 4},
wantBlocked: false,
}, {
name: "no_match_cidr",
wantRule: "",
ip: net.IP{9, 6, 7, 100},
wantBlocked: false,
}}
t.Run("allow", func(t *testing.T) {
for _, tc := range testCases {
blocked, rule := allowCtx.isBlockedIP(tc.ip)
assert.Equal(t, !tc.wantBlocked, blocked)
assert.Equal(t, tc.wantRule, rule)
}
})
t.Run("block", func(t *testing.T) {
for _, tc := range testCases {
blocked, rule := blockCtx.isBlockedIP(tc.ip)
assert.Equal(t, tc.wantBlocked, blocked)
assert.Equal(t, tc.wantRule, rule)
}
})
}

View File

@@ -2,6 +2,7 @@ package dnsforward
import (
"crypto/tls"
"encoding/binary"
"fmt"
"path"
"strings"
@@ -50,15 +51,15 @@ func clientIDFromClientServerName(hostSrvName, cliSrvName string, strict bool) (
return clientID, nil
}
// processClientIDHTTPS extracts the client's ID from the path of the
// clientIDFromDNSContextHTTPS extracts the client's ID from the path of the
// client's DNS-over-HTTPS request.
func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) {
pctx := ctx.proxyCtx
func clientIDFromDNSContextHTTPS(pctx *proxy.DNSContext) (clientID string, err error) {
r := pctx.HTTPRequest
if r == nil {
ctx.err = fmt.Errorf("proxy ctx http request of proto %s is nil", pctx.Proto)
return resultCodeError
return "", fmt.Errorf(
"proxy ctx http request of proto %s is nil",
pctx.Proto,
)
}
origPath := r.URL.Path
@@ -68,34 +69,25 @@ func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) {
}
if len(parts) == 0 || parts[0] != "dns-query" {
ctx.err = fmt.Errorf("client id check: invalid path %q", origPath)
return resultCodeError
return "", fmt.Errorf("client id check: invalid path %q", origPath)
}
clientID := ""
switch len(parts) {
case 1:
// Just /dns-query, no client ID.
return resultCodeSuccess
return "", nil
case 2:
clientID = parts[1]
default:
ctx.err = fmt.Errorf("client id check: invalid path %q: extra parts", origPath)
return resultCodeError
return "", fmt.Errorf("client id check: invalid path %q: extra parts", origPath)
}
err := ValidateClientID(clientID)
err = ValidateClientID(clientID)
if err != nil {
ctx.err = fmt.Errorf("client id check: %w", err)
return resultCodeError
return "", fmt.Errorf("client id check: %w", err)
}
ctx.clientID = clientID
return resultCodeSuccess
return clientID, nil
}
// tlsConn is a narrow interface for *tls.Conn to simplify testing.
@@ -108,53 +100,73 @@ type quicSession interface {
ConnectionState() (cs quic.ConnectionState)
}
// processClientID extracts the client's ID from the server name of the client's
// DOT or DOQ request or the path of the client's DOH.
func processClientID(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx
// clientIDFromDNSContext extracts the client's ID from the server name of the
// client's DoT or DoQ request or the path of the client's DoH. If the protocol
// is not one of these, clientID is an empty string and err is nil.
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
proto := pctx.Proto
if proto == proxy.ProtoHTTPS {
return processClientIDHTTPS(dctx)
return clientIDFromDNSContextHTTPS(pctx)
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
return resultCodeSuccess
return "", nil
}
srvConf := dctx.srv.conf
hostSrvName := srvConf.TLSConfig.ServerName
hostSrvName := s.conf.ServerName
if hostSrvName == "" {
return resultCodeSuccess
return "", nil
}
cliSrvName := ""
if proto == proxy.ProtoTLS {
switch proto {
case proxy.ProtoTLS:
conn := pctx.Conn
tc, ok := conn.(tlsConn)
if !ok {
dctx.err = fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
return resultCodeError
return "", fmt.Errorf(
"proxy ctx conn of proto %s is %T, want *tls.Conn",
proto,
conn,
)
}
cliSrvName = tc.ConnectionState().ServerName
} else if proto == proxy.ProtoQUIC {
case proxy.ProtoQUIC:
qs, ok := pctx.QUICSession.(quicSession)
if !ok {
dctx.err = fmt.Errorf("proxy ctx quic session of proto %s is %T, want quic.Session", proto, pctx.QUICSession)
return resultCodeError
return "", fmt.Errorf(
"proxy ctx quic session of proto %s is %T, want quic.Session",
proto,
pctx.QUICSession,
)
}
cliSrvName = qs.ConnectionState().TLS.ServerName
}
clientID, err := clientIDFromClientServerName(hostSrvName, cliSrvName, srvConf.StrictSNICheck)
clientID, err = clientIDFromClientServerName(
hostSrvName,
cliSrvName,
s.conf.StrictSNICheck,
)
if err != nil {
dctx.err = fmt.Errorf("client id check: %w", err)
return resultCodeError
return "", fmt.Errorf("client id check: %w", err)
}
dctx.clientID = clientID
return clientID, nil
}
// processClientID puts the clientID into the DNS context, if there is one.
func (s *Server) processClientID(dctx *dnsContext) (rc resultCode) {
pctx := dctx.proxyCtx
var key [8]byte
binary.BigEndian.PutUint64(key[:], pctx.RequestID)
clientIDData := s.clientIDCache.Get(key[:])
if clientIDData == nil {
return resultCodeSuccess
}
dctx.clientID = string(clientIDData)
return resultCodeSuccess
}

View File

@@ -45,15 +45,14 @@ func (c testQUICSession) ConnectionState() (cs quic.ConnectionState) {
return cs
}
func TestProcessClientID(t *testing.T) {
func TestServer_clientIDFromDNSContext(t *testing.T) {
testCases := []struct {
name string
proto string
proto proxy.Proto
hostSrvName string
cliSrvName string
wantClientID string
wantErrMsg string
wantRes resultCode
strictSNI bool
}{{
name: "udp",
@@ -62,7 +61,6 @@ func TestProcessClientID(t *testing.T) {
cliSrvName: "",
wantClientID: "",
wantErrMsg: "",
wantRes: resultCodeSuccess,
strictSNI: false,
}, {
name: "tls_no_client_id",
@@ -71,7 +69,6 @@ func TestProcessClientID(t *testing.T) {
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: "",
wantRes: resultCodeSuccess,
strictSNI: true,
}, {
name: "tls_no_client_server_name",
@@ -81,7 +78,6 @@ func TestProcessClientID(t *testing.T) {
wantClientID: "",
wantErrMsg: `client id check: client server name "" ` +
`doesn't match host server name "example.com"`,
wantRes: resultCodeError,
strictSNI: true,
}, {
name: "tls_no_client_server_name_no_strict",
@@ -90,7 +86,6 @@ func TestProcessClientID(t *testing.T) {
cliSrvName: "",
wantClientID: "",
wantErrMsg: "",
wantRes: resultCodeSuccess,
strictSNI: false,
}, {
name: "tls_client_id",
@@ -99,7 +94,6 @@ func TestProcessClientID(t *testing.T) {
cliSrvName: "cli.example.com",
wantClientID: "cli",
wantErrMsg: "",
wantRes: resultCodeSuccess,
strictSNI: true,
}, {
name: "tls_client_id_hostname_error",
@@ -109,7 +103,6 @@ func TestProcessClientID(t *testing.T) {
wantClientID: "",
wantErrMsg: `client id check: client server name "cli.example.net" ` +
`doesn't match host server name "example.com"`,
wantRes: resultCodeError,
strictSNI: true,
}, {
name: "tls_invalid_client_id",
@@ -119,7 +112,6 @@ func TestProcessClientID(t *testing.T) {
wantClientID: "",
wantErrMsg: `client id check: invalid client id "!!!": ` +
`invalid char '!' at index 0`,
wantRes: resultCodeError,
strictSNI: true,
}, {
name: "tls_client_id_too_long",
@@ -131,7 +123,6 @@ func TestProcessClientID(t *testing.T) {
wantErrMsg: `client id check: invalid client id "abcdefghijklmno` +
`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789": ` +
`label is too long, max: 63`,
wantRes: resultCodeError,
strictSNI: true,
}, {
name: "quic_client_id",
@@ -140,7 +131,6 @@ func TestProcessClientID(t *testing.T) {
cliSrvName: "cli.example.com",
wantClientID: "cli",
wantErrMsg: "",
wantRes: resultCodeSuccess,
strictSNI: true,
}}
@@ -150,6 +140,7 @@ func TestProcessClientID(t *testing.T) {
ServerName: tc.hostSrvName,
StrictSNICheck: tc.strictSNI,
}
srv := &Server{
conf: ServerConfig{TLSConfig: tlsConf},
}
@@ -168,79 +159,68 @@ func TestProcessClientID(t *testing.T) {
}
}
dctx := &dnsContext{
srv: srv,
proxyCtx: &proxy.DNSContext{
Proto: tc.proto,
Conn: conn,
QUICSession: qs,
},
pctx := &proxy.DNSContext{
Proto: tc.proto,
Conn: conn,
QUICSession: qs,
}
res := processClientID(dctx)
assert.Equal(t, tc.wantRes, res)
assert.Equal(t, tc.wantClientID, dctx.clientID)
clientID, err := srv.clientIDFromDNSContext(pctx)
assert.Equal(t, tc.wantClientID, clientID)
if tc.wantErrMsg == "" {
assert.NoError(t, dctx.err)
assert.NoError(t, err)
} else {
require.Error(t, dctx.err)
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
require.Error(t, err)
assert.Equal(t, tc.wantErrMsg, err.Error())
}
})
}
}
func TestProcessClientID_https(t *testing.T) {
func TestClientIDFromDNSContextHTTPS(t *testing.T) {
testCases := []struct {
name string
path string
wantClientID string
wantErrMsg string
wantRes resultCode
}{{
name: "no_client_id",
path: "/dns-query",
wantClientID: "",
wantErrMsg: "",
wantRes: resultCodeSuccess,
}, {
name: "no_client_id_slash",
path: "/dns-query/",
wantClientID: "",
wantErrMsg: "",
wantRes: resultCodeSuccess,
}, {
name: "client_id",
path: "/dns-query/cli",
wantClientID: "cli",
wantErrMsg: "",
wantRes: resultCodeSuccess,
}, {
name: "client_id_slash",
path: "/dns-query/cli/",
wantClientID: "cli",
wantErrMsg: "",
wantRes: resultCodeSuccess,
}, {
name: "bad_url",
path: "/foo",
wantClientID: "",
wantErrMsg: `client id check: invalid path "/foo"`,
wantRes: resultCodeError,
}, {
name: "extra",
path: "/dns-query/cli/foo",
wantClientID: "",
wantErrMsg: `client id check: invalid path "/dns-query/cli/foo": extra parts`,
wantRes: resultCodeError,
}, {
name: "invalid_client_id",
path: "/dns-query/!!!",
wantClientID: "",
wantErrMsg: `client id check: invalid client id "!!!": ` +
`invalid char '!' at index 0`,
wantRes: resultCodeError,
}}
for _, tc := range testCases {
@@ -251,23 +231,20 @@ func TestProcessClientID_https(t *testing.T) {
},
}
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
Proto: proxy.ProtoHTTPS,
HTTPRequest: r,
},
pctx := &proxy.DNSContext{
Proto: proxy.ProtoHTTPS,
HTTPRequest: r,
}
res := processClientID(dctx)
assert.Equal(t, tc.wantRes, res)
assert.Equal(t, tc.wantClientID, dctx.clientID)
clientID, err := clientIDFromDNSContextHTTPS(pctx)
assert.Equal(t, tc.wantClientID, clientID)
if tc.wantErrMsg == "" {
assert.NoError(t, dctx.err)
assert.NoError(t, err)
} else {
require.Error(t, dctx.err)
require.Error(t, err)
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
assert.Equal(t, tc.wantErrMsg, err.Error())
}
})
}

View File

@@ -21,6 +21,30 @@ import (
"github.com/ameshkov/dnscrypt/v2"
)
// BlockingMode is an enum of all allowed blocking modes.
type BlockingMode string
// Allowed blocking modes.
const (
// BlockingModeCustomIP means respond with a custom IP address.
BlockingModeCustomIP BlockingMode = "custom_ip"
// BlockingModeDefault is the same as BlockingModeNullIP for
// Adblock-style rules, but responds with the IP address specified in
// the rule when blocked by an `/etc/hosts`-style rule.
BlockingModeDefault BlockingMode = "default"
// BlockingModeNullIP means respond with a zero IP address: "0.0.0.0"
// for A requests and "::" for AAAA ones.
BlockingModeNullIP BlockingMode = "null_ip"
// BlockingModeNXDOMAIN means respond with the NXDOMAIN code.
BlockingModeNXDOMAIN BlockingMode = "nxdomain"
// BlockingModeREFUSED means respond with the REFUSED code.
BlockingModeREFUSED BlockingMode = "refused"
)
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct {
@@ -38,11 +62,11 @@ type FilteringConfig struct {
// Protection configuration
// --
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of filtering features
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockingIPv4 net.IP `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request
BlockingIPv6 net.IP `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of filtering features
BlockingMode BlockingMode `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockingIPv4 net.IP `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request
BlockingIPv6 net.IP `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
// IP (or domain name) which is used to respond to DNS requests blocked by parental control or safe-browsing
ParentalBlockHost string `yaml:"parental_block_host"`
@@ -87,10 +111,12 @@ type FilteringConfig struct {
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
// IPSET configuration - add IP addresses of the specified domain names to an ipset list
// Syntax:
// "DOMAIN[,DOMAIN].../IPSET_NAME"
IPSETList []string `yaml:"ipset"`
// IpsetList is the ipset configuration that allows AdGuard Home to add
// IP addresses of the specified domain names to an ipset list. Syntax:
//
// DOMAIN[,DOMAIN].../IPSET_NAME
//
IpsetList []string `yaml:"ipset"`
}
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
@@ -141,7 +167,7 @@ type ServerConfig struct {
FilteringConfig
TLSConfig
DNSCryptConfig
TLSAllowUnencryptedDOH bool
TLSAllowUnencryptedDoH bool
// UpstreamTimeout is the timeout for querying upstream servers.
UpstreamTimeout time.Duration
@@ -305,7 +331,7 @@ func (s *Server) prepareUpstreamSettings() error {
upstreams = aghstrings.FilterOut(upstreams, aghstrings.IsCommentOrEmpty)
upstreamConfig, err := proxy.ParseUpstreamsConfig(
upstreams,
upstream.Options{
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
},
@@ -316,10 +342,10 @@ func (s *Server) prepareUpstreamSettings() error {
if len(upstreamConfig.Upstreams) == 0 {
log.Info("warning: no default upstream servers specified, using %v", defaultDNS)
var uc proxy.UpstreamConfig
var uc *proxy.UpstreamConfig
uc, err = proxy.ParseUpstreamsConfig(
defaultDNS,
upstream.Options{
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
},
@@ -330,7 +356,8 @@ func (s *Server) prepareUpstreamSettings() error {
upstreamConfig.Upstreams = uc.Upstreams
}
s.conf.UpstreamConfig = &upstreamConfig
s.conf.UpstreamConfig = upstreamConfig
return nil
}

View File

@@ -32,7 +32,7 @@ type dnsContext struct {
unreversedReqIP net.IP
// err is the error returned from a processing function.
err error
// clientID is the clientID from DOH, DOQ, or DOT, if provided.
// clientID is the clientID from DoH, DoQ, or DoT, if provided.
clientID string
// origQuestion is the question received from the client. It is set
// when the request is modified by rewrites.
@@ -89,7 +89,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
s.processInternalHosts,
s.processRestrictLocal,
s.processInternalIPAddrs,
processClientID,
s.processClientID,
processFilteringBeforeRequest,
s.processLocalPTR,
s.processUpstream,
@@ -165,7 +165,7 @@ func (s *Server) setTableHostToIP(t hostToIPTable) {
s.tableHostToIP = t
}
func (s *Server) setTableIPToHost(t ipToHostTable) {
func (s *Server) setTableIPToHost(t *aghnet.IPMap) {
s.tableIPToHostLock.Lock()
defer s.tableIPToHostLock.Unlock()
@@ -188,13 +188,13 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
}
var hostToIP hostToIPTable
var ipToHost ipToHostTable
var ipToHost *aghnet.IPMap
if add {
hostToIP = make(hostToIPTable)
ipToHost = make(ipToHostTable)
ll := s.dhcpServer.Leases(dhcpd.LeasesAll)
hostToIP = make(hostToIPTable, len(ll))
ipToHost = aghnet.NewIPMap(len(ll))
for _, l := range ll {
// TODO(a.garipov): Remove this after we're finished
// with the client hostname validations in the DHCP
@@ -210,14 +210,14 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
lowhost := strings.ToLower(l.Hostname)
ipToHost[l.IP.String()] = lowhost
ipToHost.Set(l.IP, lowhost)
ip := make(net.IP, 4)
copy(ip, l.IP.To4())
hostToIP[lowhost] = ip
}
log.Debug("dns: added %d A/PTR entries from DHCP", len(ipToHost))
log.Debug("dns: added %d A/PTR entries from DHCP", ipToHost.Len())
}
s.setTableHostToIP(hostToIP)
@@ -377,9 +377,19 @@ func (s *Server) ipToHost(ip net.IP) (host string, ok bool) {
return "", false
}
host, ok = s.tableIPToHost[ip.String()]
var v interface{}
v, ok = s.tableIPToHost.Get(ip)
if !ok {
return "", false
}
return host, ok
if host, ok = v.(string); !ok {
log.Error("dns: bad type %T in tableIPToHost for %s", v, ip)
return "", false
}
return host, true
}
// Respond to PTR requests if the target IP is leased by our DHCP server and the

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"net"
"net/http"
"os"
"runtime"
"strings"
"sync"
@@ -19,6 +18,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
@@ -27,6 +27,11 @@ import (
// DefaultTimeout is the default upstream timeout
const DefaultTimeout = 10 * time.Second
// defaultClientIDCacheCount is the default count of items in the LRU client ID
// cache. The assumption here is that there won't be more than this many
// requests between the BeforeRequestHandler stage and the actual processing.
const defaultClientIDCacheCount = 1024
const (
safeBrowsingBlockHost = "standard-block.dns.adguard.com"
parentalBlockHost = "family-block.dns.adguard.com"
@@ -45,12 +50,6 @@ var webRegistered bool
// hostToIPTable is an alias for the type of Server.tableHostToIP.
type hostToIPTable = map[string]net.IP
// ipToHostTable is an alias for the type of Server.tableIPToHost.
//
// TODO(a.garipov): Define an IPMap type in aghnet and use here and in other
// places?
type ipToHostTable = map[string]string
// Server is the main way to start a DNS server.
//
// Example:
@@ -82,9 +81,13 @@ type Server struct {
tableHostToIP hostToIPTable
tableHostToIPLock sync.Mutex
tableIPToHost ipToHostTable
tableIPToHost *aghnet.IPMap
tableIPToHostLock sync.Mutex
// clientIDCache is a temporary storage for clientIDs that were
// extracted during the BeforeRequestHandler stage.
clientIDCache cache.Cache
// DNS proxy instance for internal usage
// We don't Start() it and so no listen port is required.
internalProxy *proxy.Proxy
@@ -153,6 +156,10 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
subnetDetector: p.SubnetDetector,
localDomainSuffix: localDomainSuffix,
recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum),
clientIDCache: cache.New(cache.Config{
EnableLRU: true,
MaxCount: defaultClientIDCacheCount,
}),
}
// TODO(e.burkov): Enable the refresher after the actual implementation
@@ -203,7 +210,7 @@ func (s *Server) Close() {
s.queryLog = nil
s.dnsProxy = nil
if err := s.ipset.Close(); err != nil {
if err := s.ipset.close(); err != nil {
log.Error("closing ipset: %s", err)
}
}
@@ -415,19 +422,22 @@ func (s *Server) setupResolvers(localAddrs []string) (err error) {
log.Debug("upstreams to resolve PTR for local addresses: %v", localAddrs)
var upsConfig proxy.UpstreamConfig
upsConfig, err = proxy.ParseUpstreamsConfig(localAddrs, upstream.Options{
Bootstrap: bootstraps,
Timeout: defaultLocalTimeout,
// TODO(e.burkov): Should we verify server's ceritificates?
})
var upsConfig *proxy.UpstreamConfig
upsConfig, err = proxy.ParseUpstreamsConfig(
localAddrs,
&upstream.Options{
Bootstrap: bootstraps,
Timeout: defaultLocalTimeout,
// TODO(e.burkov): Should we verify server's ceritificates?
},
)
if err != nil {
return fmt.Errorf("parsing upstreams: %w", err)
}
s.localResolvers = &proxy.Proxy{
Config: proxy.Config{
UpstreamConfig: &upsConfig,
UpstreamConfig: upsConfig,
},
}
@@ -451,26 +461,15 @@ func (s *Server) Prepare(config *ServerConfig) error {
// --
s.initDefaultSettings()
// Initialize IPSET configuration
// Initialize ipset configuration
// --
err := s.ipset.init(s.conf.IPSETList)
err := s.ipset.init(s.conf.IpsetList)
if err != nil {
if !errors.Is(err, os.ErrInvalid) && !errors.Is(err, os.ErrPermission) {
return fmt.Errorf("cannot initialize ipset: %w", err)
}
// ipset cannot currently be initialized if the server was
// installed from Snap or when the user or the binary doesn't
// have the required permissions, or when the kernel doesn't
// support netfilter.
//
// Log and go on.
//
// TODO(a.garipov): The Snap problem can probably be solved if
// we add the netlink-connector interface plug.
log.Info("warning: cannot initialize ipset: %s", err)
return err
}
log.Debug("inited ipset")
// Prepare DNS servers settings
// --
err = s.prepareUpstreamSettings()
@@ -589,11 +588,33 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// IsBlockedIP - return TRUE if this client should be blocked
func (s *Server) IsBlockedIP(ip net.IP) (bool, string) {
if ip == nil {
return false, ""
// IsBlockedClient returns true if the client is blocked by the current access
// settings.
func (s *Server) IsBlockedClient(ip net.IP, clientID string) (blocked bool, rule string) {
s.serverLock.RLock()
defer s.serverLock.RUnlock()
allowlistMode := s.access.allowlistMode()
blockedByIP, rule := s.access.isBlockedIP(ip)
blockedByClientID := s.access.isBlockedClientID(clientID)
// Allow if at least one of the checks allows in allowlist mode, but
// block if at least one of the checks blocks in blocklist mode.
if allowlistMode && blockedByIP && blockedByClientID {
log.Debug("client %s (id %q) is not in access allowlist", ip, clientID)
// Return now without substituting the empty rule for the
// clientID because the rule can't be empty here.
return true, rule
} else if !allowlistMode && (blockedByIP || blockedByClientID) {
log.Debug("client %s (id %q) is in access blocklist", ip, clientID)
blocked = true
}
return s.access.IsBlockedIP(ip)
if rule == "" {
rule = clientID
}
return blocked, rule
}

View File

@@ -257,19 +257,22 @@ func TestServer(t *testing.T) {
testCases := []struct {
name string
proto string
net string
proto proxy.Proto
}{{
name: "message_over_udp",
net: "",
proto: proxy.ProtoUDP,
}, {
name: "message_over_tcp",
net: "tcp",
proto: proxy.ProtoTCP,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
addr := s.dnsProxy.Addr(tc.proto)
client := dns.Client{Net: tc.proto}
client := dns.Client{Net: tc.net}
reply, _, err := client.Exchange(createGoogleATestMessage(), addr.String())
require.NoErrorf(t, err, "сouldn't talk to server %s: %s", addr, err)
@@ -324,7 +327,7 @@ func TestServerWithProtectionDisabled(t *testing.T) {
// Message over UDP.
req := createGoogleATestMessage()
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
client := dns.Client{Net: proxy.ProtoUDP}
client := &dns.Client{}
reply, _, err := client.Exchange(req, addr.String())
require.NoErrorf(t, err, "сouldn't talk to server %s: %s", addr, err)
@@ -376,7 +379,7 @@ func TestDoQServer(t *testing.T) {
// Create a DNS-over-QUIC upstream.
addr := s.dnsProxy.Addr(proxy.ProtoQUIC)
opts := upstream.Options{InsecureSkipVerify: true}
opts := &upstream.Options{InsecureSkipVerify: true}
u, err := upstream.AddressToUpstream(fmt.Sprintf("%s://%s", proxy.ProtoQUIC, addr), opts)
require.NoError(t, err)
@@ -420,7 +423,7 @@ func TestServerRace(t *testing.T) {
// Message over UDP.
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
conn, err := dns.Dial(proxy.ProtoUDP, addr.String())
conn, err := dns.Dial("udp", addr.String())
require.NoErrorf(t, err, "cannot connect to the proxy: %s", err)
sendTestMessagesAsync(t, conn)
@@ -445,7 +448,7 @@ func TestSafeSearch(t *testing.T) {
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
client := dns.Client{Net: proxy.ProtoUDP}
client := &dns.Client{}
yandexIP := net.IP{213, 180, 193, 56}
googleIP, _ := resolver.HostToIPs("forcesafesearch.google.com")
@@ -507,7 +510,6 @@ func TestInvalidRequest(t *testing.T) {
// Send a DNS request without question.
_, _, err := (&dns.Client{
Net: proxy.ProtoUDP,
Timeout: 500 * time.Millisecond,
}).Exchange(&req, addr)
@@ -520,6 +522,7 @@ func TestBlockedRequest(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: BlockingModeDefault,
},
}
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
@@ -622,6 +625,7 @@ func TestBlockCNAME(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: BlockingModeDefault,
},
}
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
@@ -724,7 +728,7 @@ func TestNullBlockedRequest(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: "null_ip",
BlockingMode: BlockingModeNullIP,
},
}
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
@@ -777,7 +781,7 @@ func TestBlockedCustomIP(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: "custom_ip",
BlockingMode: BlockingModeCustomIP,
BlockingIPv4: nil,
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
},
@@ -827,6 +831,7 @@ func TestBlockedByHosts(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: BlockingModeDefault,
},
}
@@ -993,15 +998,14 @@ type testDHCP struct{}
func (d *testDHCP) Enabled() (ok bool) { return true }
func (d *testDHCP) Leases(flags int) []dhcpd.Lease {
l := dhcpd.Lease{
func (d *testDHCP) Leases(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
return []*dhcpd.Lease{{
IP: net.IP{192, 168, 12, 34},
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
Hostname: "myhost",
}
return []dhcpd.Lease{l}
}}
}
func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {}
func TestPTRResponseFromDHCPLeases(t *testing.T) {

View File

@@ -1,6 +1,7 @@
package dnsforward
import (
"encoding/binary"
"fmt"
"strings"
@@ -11,23 +12,39 @@ import (
"github.com/miekg/dns"
)
func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool, error) {
ip := aghnet.IPFromAddr(d.Addr)
disallowed, _ := s.access.IsBlockedIP(ip)
if disallowed {
log.Tracef("Client IP %s is blocked by settings", ip)
// beforeRequestHandler is the handler that is called before any other
// processing, including logs. It performs access checks and puts the client
// ID, if there is one, into the server's cache.
func (s *Server) beforeRequestHandler(
_ *proxy.Proxy,
pctx *proxy.DNSContext,
) (reply bool, err error) {
ip := aghnet.IPFromAddr(pctx.Addr)
clientID, err := s.clientIDFromDNSContext(pctx)
if err != nil {
return false, fmt.Errorf("getting clientid: %w", err)
}
blocked, _ := s.IsBlockedClient(ip, clientID)
if blocked {
return false, nil
}
if len(d.Req.Question) == 1 {
host := strings.TrimSuffix(d.Req.Question[0].Name, ".")
if s.access.IsBlockedDomain(host) {
log.Tracef("domain %s is blocked by access settings", host)
if len(pctx.Req.Question) == 1 {
host := strings.TrimSuffix(pctx.Req.Question[0].Name, ".")
if s.access.isBlockedHost(host) {
log.Debug("host %s is in access blocklist", host)
return false, nil
}
}
if clientID != "" {
key := [8]byte{}
binary.BigEndian.PutUint64(key[:], pctx.RequestID)
s.clientIDCache.Set(key[:], []byte(clientID))
}
return true, nil
}

View File

@@ -29,21 +29,21 @@ type dnsConfig struct {
UpstreamsFile *string `json:"upstream_dns_file"`
Bootstraps *[]string `json:"bootstrap_dns"`
ProtectionEnabled *bool `json:"protection_enabled"`
RateLimit *uint32 `json:"ratelimit"`
BlockingMode *string `json:"blocking_mode"`
BlockingIPv4 net.IP `json:"blocking_ipv4"`
BlockingIPv6 net.IP `json:"blocking_ipv6"`
EDNSCSEnabled *bool `json:"edns_cs_enabled"`
DNSSECEnabled *bool `json:"dnssec_enabled"`
DisableIPv6 *bool `json:"disable_ipv6"`
UpstreamMode *string `json:"upstream_mode"`
CacheSize *uint32 `json:"cache_size"`
CacheMinTTL *uint32 `json:"cache_ttl_min"`
CacheMaxTTL *uint32 `json:"cache_ttl_max"`
ResolveClients *bool `json:"resolve_clients"`
UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
ProtectionEnabled *bool `json:"protection_enabled"`
RateLimit *uint32 `json:"ratelimit"`
BlockingMode *BlockingMode `json:"blocking_mode"`
BlockingIPv4 net.IP `json:"blocking_ipv4"`
BlockingIPv6 net.IP `json:"blocking_ipv6"`
EDNSCSEnabled *bool `json:"edns_cs_enabled"`
DNSSECEnabled *bool `json:"dnssec_enabled"`
DisableIPv6 *bool `json:"disable_ipv6"`
UpstreamMode *string `json:"upstream_mode"`
CacheSize *uint32 `json:"cache_size"`
CacheMinTTL *uint32 `json:"cache_ttl_min"`
CacheMaxTTL *uint32 `json:"cache_ttl_max"`
ResolveClients *bool `json:"resolve_clients"`
UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
}
func (s *Server) getDNSConfig() dnsConfig {
@@ -126,27 +126,17 @@ func (req *dnsConfig) checkBlockingMode() bool {
return true
}
bm := *req.BlockingMode
if bm == "custom_ip" {
if req.BlockingIPv4.To4() == nil {
return false
}
return req.BlockingIPv6 != nil
switch bm := *req.BlockingMode; bm {
case BlockingModeDefault,
BlockingModeREFUSED,
BlockingModeNXDOMAIN,
BlockingModeNullIP:
return true
case BlockingModeCustomIP:
return req.BlockingIPv4.To4() != nil && req.BlockingIPv6 != nil
default:
return false
}
for _, valid := range []string{
"default",
"refused",
"nxdomain",
"null_ip",
} {
if bm == valid {
return true
}
}
return false
}
func (req *dnsConfig) checkUpstreamsMode() bool {
@@ -177,7 +167,7 @@ func (req *dnsConfig) checkBootstrap() (string, error) {
return boot, fmt.Errorf("invalid bootstrap server address: empty")
}
if _, err := upstream.NewResolver(boot, upstream.Options{Timeout: 0}); err != nil {
if _, err := upstream.NewResolver(boot, nil); err != nil {
return boot, fmt.Errorf("invalid bootstrap server address: %w", err)
}
}
@@ -358,7 +348,7 @@ func ValidateUpstreams(upstreams []string) (err error) {
_, err = proxy.ParseUpstreamsConfig(
upstreams,
upstream.Options{
&upstream.Options{
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
@@ -556,7 +546,7 @@ func checkDNS(input string, bootstrap []string, timeout time.Duration, ef excFun
log.Debug("checking if dns server %q works...", input)
var u upstream.Upstream
u, err = upstream.AddressToUpstream(input, upstream.Options{
u, err = upstream.AddressToUpstream(input, &upstream.Options{
Bootstrap: bootstrap,
Timeout: timeout,
})
@@ -631,11 +621,11 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
// Control flow:
// web
// -> dnsforward.handleDOH -> dnsforward.ServeHTTP
// -> dnsforward.handleDoH -> dnsforward.ServeHTTP
// -> proxy.ServeHTTP -> proxy.handleDNSRequest
// -> dnsforward.handleDNSRequest
func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDOH && r.TLS == nil {
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
httpError(r, w, http.StatusNotFound, "Not Found")
return
}
@@ -663,6 +653,6 @@ func (s *Server) registerHandlers() {
// See go doc net/http.ServeMux.
//
// See also https://github.com/AdguardTeam/AdGuardHome/issues/2628.
s.conf.HTTPRegister("", "/dns-query", s.handleDOH)
s.conf.HTTPRegister("", "/dns-query/", s.handleDOH)
s.conf.HTTPRegister("", "/dns-query", s.handleDoH)
s.conf.HTTPRegister("", "/dns-query/", s.handleDoH)
}

View File

@@ -0,0 +1,137 @@
package dnsforward
import (
"fmt"
"net"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// ipsetCtx is the ipset context. ipsetMgr can be nil.
type ipsetCtx struct {
ipsetMgr aghnet.IpsetManager
}
// init initializes the ipset context. It is not safe for concurrent use.
//
// TODO(a.garipov): Rewrite into a simple constructor?
func (c *ipsetCtx) init(ipsetConf []string) (err error) {
c.ipsetMgr, err = aghnet.NewIpsetManager(ipsetConf)
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrPermission) {
// ipset cannot currently be initialized if the server was
// installed from Snap or when the user or the binary doesn't
// have the required permissions, or when the kernel doesn't
// support netfilter.
//
// Log and go on.
//
// TODO(a.garipov): The Snap problem can probably be solved if
// we add the netlink-connector interface plug.
log.Info("warning: cannot initialize ipset: %s", err)
return nil
} else if unsupErr := (&aghos.UnsupportedError{}); errors.As(err, &unsupErr) {
log.Info("warning: %s", err)
return nil
} else if err != nil {
return fmt.Errorf("initializing ipset: %w", err)
}
return nil
}
// close closes the Linux Netfilter connections.
func (c *ipsetCtx) close() (err error) {
if c.ipsetMgr != nil {
return c.ipsetMgr.Close()
}
return nil
}
func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
return dctx != nil &&
dctx.responseFromUpstream &&
dctx.proxyCtx != nil &&
dctx.proxyCtx.Res != nil &&
dctx.proxyCtx.Req != nil &&
len(dctx.proxyCtx.Req.Question) > 0
}
// skipIpsetProcessing returns true when the ipset processing can be skipped for
// this request.
func (c *ipsetCtx) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
if c == nil || c.ipsetMgr == nil || !c.dctxIsfilled(dctx) {
return true
}
qtype := dctx.proxyCtx.Req.Question[0].Qtype
return qtype != dns.TypeA && qtype != dns.TypeAAAA && qtype != dns.TypeANY
}
// ipFromRR returns an IP address from a DNS resource record.
func ipFromRR(rr dns.RR) (ip net.IP) {
switch a := rr.(type) {
case *dns.A:
return a.A
case *dns.AAAA:
return a.AAAA
default:
return nil
}
}
// ipsFromAnswer returns IPv4 and IPv6 addresses from a DNS answer.
func ipsFromAnswer(ans []dns.RR) (ip4s, ip6s []net.IP) {
for _, rr := range ans {
ip := ipFromRR(rr)
if ip == nil {
continue
}
if ip.To4() == nil {
ip6s = append(ip6s, ip)
continue
}
ip4s = append(ip4s, ip)
}
return ip4s, ip6s
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(dctx *dnsContext) (rc resultCode) {
if c.skipIpsetProcessing(dctx) {
return resultCodeSuccess
}
log.Debug("ipset: starting processing")
req := dctx.proxyCtx.Req
host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)
n, err := c.ipsetMgr.Add(host, ip4s, ip6s)
if err != nil {
// Consider ipset errors non-critical to the request.
log.Error("ipset: adding host ips: %s", err)
return resultCodeSuccess
}
log.Debug("ipset: added %d new ips", n)
return resultCodeSuccess
}

View File

@@ -1,401 +0,0 @@
//go:build linux
// +build linux
package dnsforward
import (
"fmt"
"net"
"strings"
"sync"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/miekg/dns"
"github.com/ti-mo/netfilter"
)
// TODO(a.garipov): Cover with unit tests as well as document how to test it
// manually. The original PR by @dsheets on Github contained an integration
// test, but unfortunately I didn't have the time to properly refactor it and
// check it in.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2611.
// ipsetProps contains one Linux Netfilter ipset properties.
type ipsetProps struct {
name string
family netfilter.ProtoFamily
}
// ipsetCtx is the Linux Netfilter ipset context.
type ipsetCtx struct {
// mu protects all properties below.
mu *sync.Mutex
nameToIpset map[string]ipsetProps
domainToIpsets map[string][]ipsetProps
// TODO(a.garipov): Currently, the ipset list is static, and we don't
// read the IPs already in sets, so we can assume that all incoming IPs
// are either added to all corresponding ipsets or not. When that stops
// being the case, for example if we add dynamic reconfiguration of
// ipsets, this map will need to become a per-ipset-name one.
addedIPs map[[16]byte]struct{}
ipv4Conn *ipset.Conn
ipv6Conn *ipset.Conn
}
// dialNetfilter establishes connections to Linux's netfilter module.
func (c *ipsetCtx) dialNetfilter(config *netlink.Config) (err error) {
// The kernel API does not actually require two sockets but package
// github.com/digineo/go-ipset does.
//
// TODO(a.garipov): Perhaps we can ditch package ipset altogether and
// just use packages netfilter and netlink.
c.ipv4Conn, err = ipset.Dial(netfilter.ProtoIPv4, config)
if err != nil {
return fmt.Errorf("dialing v4: %w", err)
}
c.ipv6Conn, err = ipset.Dial(netfilter.ProtoIPv6, config)
if err != nil {
return fmt.Errorf("dialing v6: %w", err)
}
return nil
}
// ipsetProps returns the properties of an ipset with the given name.
func (c *ipsetCtx) ipsetProps(name string) (set ipsetProps, err error) {
// The family doesn't seem to matter when we use a header query, so
// query only the IPv4 one.
//
// TODO(a.garipov): Find out if this is a bug or a feature.
var res *ipset.HeaderPolicy
res, err = c.ipv4Conn.Header(name)
if err != nil {
return set, err
}
if res == nil || res.Family == nil {
return set, errors.Error("empty response or no family data")
}
family := netfilter.ProtoFamily(res.Family.Value)
if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {
return set, fmt.Errorf("unexpected ipset family %s", family)
}
return ipsetProps{
name: name,
family: family,
}, nil
}
// ipsets returns currently known ipsets.
func (c *ipsetCtx) ipsets(names []string) (sets []ipsetProps, err error) {
for _, name := range names {
set, ok := c.nameToIpset[name]
if ok {
sets = append(sets, set)
continue
}
set, err = c.ipsetProps(name)
if err != nil {
return nil, fmt.Errorf("querying ipset %q: %w", name, err)
}
c.nameToIpset[name] = set
sets = append(sets, set)
}
return sets, nil
}
// parseIpsetConfig parses one ipset configuration string.
func parseIpsetConfig(cfgStr string) (hosts, ipsetNames []string, err error) {
cfgStr = strings.TrimSpace(cfgStr)
hostsAndNames := strings.Split(cfgStr, "/")
if len(hostsAndNames) != 2 {
return nil, nil, fmt.Errorf("invalid value %q: expected one slash", cfgStr)
}
hosts = strings.Split(hostsAndNames[0], ",")
ipsetNames = strings.Split(hostsAndNames[1], ",")
if len(ipsetNames) == 0 {
log.Info("ipset: resolutions for %q will not be stored", hosts)
return nil, nil, nil
}
for i := range ipsetNames {
ipsetNames[i] = strings.TrimSpace(ipsetNames[i])
if len(ipsetNames[i]) == 0 {
return nil, nil, fmt.Errorf("invalid value %q: empty ipset name", cfgStr)
}
}
for i := range hosts {
hosts[i] = strings.TrimSpace(hosts[i])
hosts[i] = strings.ToLower(hosts[i])
if len(hosts[i]) == 0 {
log.Info("ipset: root catchall in %q", ipsetNames)
}
}
return hosts, ipsetNames, nil
}
// init initializes the ipset context. It is not safe for concurrent use.
//
// TODO(a.garipov): Rewrite into a simple constructor?
func (c *ipsetCtx) init(ipsetConfig []string) (err error) {
c.mu = &sync.Mutex{}
c.nameToIpset = make(map[string]ipsetProps)
c.domainToIpsets = make(map[string][]ipsetProps)
c.addedIPs = make(map[[16]byte]struct{})
err = c.dialNetfilter(&netlink.Config{})
if err != nil {
return fmt.Errorf("ipset: dialing netfilter: %w", err)
}
for i, cfgStr := range ipsetConfig {
var hosts, ipsetNames []string
hosts, ipsetNames, err = parseIpsetConfig(cfgStr)
if err != nil {
return fmt.Errorf("ipset: config line at index %d: %w", i, err)
}
var ipsets []ipsetProps
ipsets, err = c.ipsets(ipsetNames)
if err != nil {
return fmt.Errorf("ipset: getting ipsets config line at index %d: %w", i, err)
}
for _, host := range hosts {
c.domainToIpsets[host] = append(c.domainToIpsets[host], ipsets...)
}
}
log.Debug("ipset: added %d domains for %d ipsets", len(c.domainToIpsets), len(c.nameToIpset))
return nil
}
// Close closes the Linux Netfilter connections.
func (c *ipsetCtx) Close() (err error) {
var errs []error
if c.ipv4Conn != nil {
err = c.ipv4Conn.Close()
if err != nil {
errs = append(errs, err)
}
}
if c.ipv6Conn != nil {
err = c.ipv6Conn.Close()
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errors.List("closing ipsets", errs...)
}
return nil
}
// ipFromRR returns an IP address from a DNS resource record.
func ipFromRR(rr dns.RR) (ip net.IP) {
switch a := rr.(type) {
case *dns.A:
return a.A
case *dns.AAAA:
return a.AAAA
default:
return nil
}
}
// lookupHost find the ipsets for the host, taking subdomain wildcards into
// account.
func (c *ipsetCtx) lookupHost(host string) (sets []ipsetProps) {
// Search for matching ipset hosts starting with most specific
// subdomain. We could use a trie here but the simple, inefficient
// solution isn't that expensive. ~75 % for 10 subdomains vs 0, but
// still sub-microsecond on a Core i7.
//
// TODO(a.garipov): Re-add benchmarks from the original PR.
for i := 0; i != -1; i++ {
host = host[i:]
sets = c.domainToIpsets[host]
if sets != nil {
return sets
}
i = strings.Index(host, ".")
if i == -1 {
break
}
}
// Check the root catch-all one.
return c.domainToIpsets[""]
}
// addIPs adds the IP addresses for the host to the ipset. set must be same
// family as set's family.
func (c *ipsetCtx) addIPs(host string, set ipsetProps, ips []net.IP) (err error) {
if len(ips) == 0 {
return
}
entries := make([]*ipset.Entry, 0, len(ips))
for _, ip := range ips {
entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))
}
var conn *ipset.Conn
switch set.family {
case netfilter.ProtoIPv4:
conn = c.ipv4Conn
case netfilter.ProtoIPv6:
conn = c.ipv6Conn
default:
return fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
}
err = conn.Add(set.name, entries...)
if err != nil {
return fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err)
}
log.Debug("ipset: added %s%s to ipset %s", host, ips, set.name)
return nil
}
// skipIpsetProcessing returns true when the ipset processing can be skipped for
// this request.
func (c *ipsetCtx) skipIpsetProcessing(ctx *dnsContext) (ok bool) {
if len(c.domainToIpsets) == 0 || ctx == nil || !ctx.responseFromUpstream {
return true
}
req := ctx.proxyCtx.Req
if req == nil || len(req.Question) == 0 {
return true
}
qt := req.Question[0].Qtype
return qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeANY
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(ctx *dnsContext) (rc resultCode) {
var err error
if c == nil {
return resultCodeSuccess
}
log.Debug("ipset: starting processing")
c.mu.Lock()
defer c.mu.Unlock()
if c.skipIpsetProcessing(ctx) {
log.Debug("ipset: skipped processing for request")
return resultCodeSuccess
}
req := ctx.proxyCtx.Req
host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
sets := c.lookupHost(host)
if len(sets) == 0 {
log.Debug("ipset: no ipsets for host %s", host)
return resultCodeSuccess
}
log.Debug("ipset: found ipsets %+v for host %s", sets, host)
if ctx.proxyCtx.Res == nil {
return resultCodeSuccess
}
ans := ctx.proxyCtx.Res.Answer
l := len(ans)
v4s := make([]net.IP, 0, l)
v6s := make([]net.IP, 0, l)
for _, rr := range ans {
ip := ipFromRR(rr)
if ip == nil {
continue
}
var iparr [16]byte
copy(iparr[:], ip.To16())
if _, added := c.addedIPs[iparr]; added {
continue
}
if ip.To4() == nil {
v6s = append(v6s, ip)
continue
}
v4s = append(v4s, ip)
}
setLoop:
for _, set := range sets {
switch set.family {
case netfilter.ProtoIPv4:
err = c.addIPs(host, set, v4s)
if err != nil {
break setLoop
}
case netfilter.ProtoIPv6:
err = c.addIPs(host, set, v6s)
if err != nil {
break setLoop
}
default:
err = fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
break setLoop
}
}
if err != nil {
log.Error("ipset: adding host ips: %s", err)
} else {
log.Debug("ipset: processed %d new ips", len(v4s)+len(v6s))
}
for _, ip := range v4s {
var iparr [16]byte
copy(iparr[:], ip.To16())
c.addedIPs[iparr] = struct{}{}
}
for _, ip := range v6s {
var iparr [16]byte
copy(iparr[:], ip.To16())
c.addedIPs[iparr] = struct{}{}
}
return resultCodeSuccess
}

View File

@@ -1,27 +0,0 @@
//go:build !linux
// +build !linux
package dnsforward
import (
"github.com/AdguardTeam/golibs/log"
)
type ipsetCtx struct{}
// init initializes the ipset context.
func (c *ipsetCtx) init(ipsetConfig []string) (err error) {
if len(ipsetConfig) != 0 {
log.Info("ipset: only available on linux")
}
return nil
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(_ *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
// Close closes the Linux Netfilter connections.
func (c *ipsetCtx) Close() (_ error) { return nil }

View File

@@ -0,0 +1,116 @@
package dnsforward
import (
"net"
"testing"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
// fakeIpsetMgr is a fake aghnet.IpsetManager for tests.
type fakeIpsetMgr struct {
ip4s []net.IP
ip6s []net.IP
}
// Add implements the aghnet.IpsetManager inteface for *fakeIpsetMgr.
func (m *fakeIpsetMgr) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
m.ip4s = append(m.ip4s, ip4s...)
m.ip6s = append(m.ip6s, ip6s...)
return len(ip4s) + len(ip6s), nil
}
// Close implements the aghnet.IpsetManager interface for *fakeIpsetMgr.
func (*fakeIpsetMgr) Close() (err error) {
return nil
}
func TestIpsetCtx_process(t *testing.T) {
ip4 := net.IP{1, 2, 3, 4}
ip6 := net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x56, 0x78,
}
req4 := createTestMessageWithType("example.com", dns.TypeA)
req6 := createTestMessageWithType("example.com", dns.TypeAAAA)
resp4 := &dns.Msg{
Answer: []dns.RR{&dns.A{
A: ip4,
}},
}
resp6 := &dns.Msg{
Answer: []dns.RR{&dns.AAAA{
AAAA: ip6,
}},
}
t.Run("nil", func(t *testing.T) {
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{},
responseFromUpstream: true,
}
ictx := &ipsetCtx{}
rc := ictx.process(dctx)
assert.Equal(t, resultCodeSuccess, rc)
err := ictx.close()
assert.NoError(t, err)
})
t.Run("ipv4", func(t *testing.T) {
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
Req: req4,
Res: resp4,
},
responseFromUpstream: true,
}
m := &fakeIpsetMgr{}
ictx := &ipsetCtx{
ipsetMgr: m,
}
rc := ictx.process(dctx)
assert.Equal(t, resultCodeSuccess, rc)
assert.Equal(t, []net.IP{ip4}, m.ip4s)
assert.Empty(t, m.ip6s)
err := ictx.close()
assert.NoError(t, err)
})
t.Run("ipv6", func(t *testing.T) {
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
Req: req6,
Res: resp6,
},
responseFromUpstream: true,
}
m := &fakeIpsetMgr{}
ictx := &ipsetCtx{
ipsetMgr: m,
}
rc := ictx.process(dctx)
assert.Equal(t, resultCodeSuccess, rc)
assert.Empty(t, m.ip4s)
assert.Equal(t, []net.IP{ip6}, m.ip6s)
err := ictx.close()
assert.NoError(t, err)
})
}

View File

@@ -26,17 +26,29 @@ func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) {
return resp
}
// ipsFromRules extracts non-IP addresses from the filtering result rules.
func ipsFromRules(resRules []*filtering.ResultRule) (ips []net.IP) {
for _, r := range resRules {
if r.IP != nil {
ips = append(ips, r.IP)
}
}
return ips
}
// genDNSFilterMessage generates a DNS message corresponding to the filtering result
func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *filtering.Result) *dns.Msg {
m := d.Req
if m.Question[0].Qtype != dns.TypeA && m.Question[0].Qtype != dns.TypeAAAA {
if s.conf.BlockingMode == "null_ip" {
if s.conf.BlockingMode == BlockingModeNullIP {
return s.makeResponse(m)
}
return s.genNXDomain(m)
}
ips := ipsFromRules(result.Rules)
switch result.Reason {
case filtering.FilteredSafeBrowsing:
return s.genBlockedHost(m, s.conf.SafeBrowsingBlockHost, d)
@@ -46,42 +58,45 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *filtering.Resu
// If the query was filtered by "Safe search", filtering also must return
// the IP address that must be used in response.
// In this case regardless of the filtering method, we should return it
if result.Reason == filtering.FilteredSafeSearch &&
len(result.Rules) > 0 &&
result.Rules[0].IP != nil {
return s.genResponseWithIP(m, result.Rules[0].IP)
if result.Reason == filtering.FilteredSafeSearch && len(ips) > 0 {
return s.genResponseWithIPs(m, ips)
}
if s.conf.BlockingMode == "null_ip" {
// it means that we should return 0.0.0.0 or :: for any blocked request
return s.makeResponseNullIP(m)
} else if s.conf.BlockingMode == "custom_ip" {
// means that we should return custom IP for any blocked request
switch s.conf.BlockingMode {
case BlockingModeCustomIP:
switch m.Question[0].Qtype {
case dns.TypeA:
return s.genARecord(m, s.conf.BlockingIPv4)
case dns.TypeAAAA:
return s.genAAAARecord(m, s.conf.BlockingIPv6)
default:
// Generally shouldn't happen, since the types
// are checked above.
log.Error(
"dns: invalid msg type %d for blocking mode %s",
m.Question[0].Qtype,
s.conf.BlockingMode,
)
return s.makeResponse(m)
}
case BlockingModeDefault:
if len(ips) > 0 {
return s.genResponseWithIPs(m, ips)
}
} else if s.conf.BlockingMode == "nxdomain" {
// means that we should return NXDOMAIN for any blocked request
return s.makeResponseNullIP(m)
case BlockingModeNullIP:
return s.makeResponseNullIP(m)
case BlockingModeNXDOMAIN:
return s.genNXDomain(m)
} else if s.conf.BlockingMode == "refused" {
// means that we should return NXDOMAIN for any blocked request
case BlockingModeREFUSED:
return s.makeResponseREFUSED(m)
}
default:
log.Error("dns: invalid blocking mode %q", s.conf.BlockingMode)
// Default blocking mode
// If there's an IP specified in the rule, return it
// For host-type rules, return null IP
if len(result.Rules) > 0 && result.Rules[0].IP != nil {
return s.genResponseWithIP(m, result.Rules[0].IP)
return s.makeResponse(m)
}
return s.makeResponseNullIP(m)
}
}
@@ -166,35 +181,60 @@ func (s *Server) genAnswerTXT(req *dns.Msg, strs []string) (ans *dns.TXT) {
}
}
// generate DNS response message with an IP address
func (s *Server) genResponseWithIP(req *dns.Msg, ip net.IP) *dns.Msg {
if req.Question[0].Qtype == dns.TypeA && ip.To4() != nil {
return s.genARecord(req, ip.To4())
} else if req.Question[0].Qtype == dns.TypeAAAA &&
len(ip) == net.IPv6len && ip.To4() == nil {
return s.genAAAARecord(req, ip)
// genResponseWithIPs generates a DNS response message with the provided IP
// addresses and an appropriate resource record type. If any of the IPs cannot
// be converted to the correct protocol, genResponseWithIPs returns an empty
// response.
func (s *Server) genResponseWithIPs(req *dns.Msg, ips []net.IP) (resp *dns.Msg) {
var ans []dns.RR
switch req.Question[0].Qtype {
case dns.TypeA:
for _, ip := range ips {
if ip4 := ip.To4(); ip4 == nil {
ans = nil
break
}
ans = append(ans, s.genAnswerA(req, ip))
}
case dns.TypeAAAA:
for _, ip := range ips {
ans = append(ans, s.genAnswerAAAA(req, ip.To16()))
}
default:
// Go on and return an empty response.
}
// empty response
resp := s.makeResponse(req)
resp = s.makeResponse(req)
resp.Answer = ans
return resp
}
// Respond with 0.0.0.0 for A, :: for AAAA, empty response for other types
func (s *Server) makeResponseNullIP(req *dns.Msg) *dns.Msg {
if req.Question[0].Qtype == dns.TypeA {
return s.genARecord(req, []byte{0, 0, 0, 0})
} else if req.Question[0].Qtype == dns.TypeAAAA {
return s.genAAAARecord(req, net.IPv6zero)
// makeResponseNullIP creates a response with 0.0.0.0 for A requests, :: for
// AAAA requests, and an empty response for other types.
func (s *Server) makeResponseNullIP(req *dns.Msg) (resp *dns.Msg) {
// Respond with the corresponding zero IP type as opposed to simply
// using one or the other in both cases, because the IPv4 zero IP is
// converted to a IPV6-mapped IPv4 address, while the IPv6 zero IP is
// converted into an empty slice instead of the zero IPv4.
switch req.Question[0].Qtype {
case dns.TypeA:
resp = s.genResponseWithIPs(req, []net.IP{{0, 0, 0, 0}})
case dns.TypeAAAA:
resp = s.genResponseWithIPs(req, []net.IP{net.IPv6zero})
default:
resp = s.makeResponse(req)
}
return s.makeResponse(req)
return resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
ip := net.ParseIP(newAddr)
if ip != nil {
return s.genResponseWithIP(request, ip)
return s.genResponseWithIPs(request, []net.IP{ip})
}
// look up the hostname, TODO: cache

View File

@@ -44,11 +44,11 @@ func processQueryLogsAndStats(ctx *dnsContext) (rc resultCode) {
switch pctx.Proto {
case proxy.ProtoHTTPS:
p.ClientProto = querylog.ClientProtoDOH
p.ClientProto = querylog.ClientProtoDoH
case proxy.ProtoQUIC:
p.ClientProto = querylog.ClientProtoDOQ
p.ClientProto = querylog.ClientProtoDoQ
case proxy.ProtoTLS:
p.ClientProto = querylog.ClientProtoDOT
p.ClientProto = querylog.ClientProtoDoT
case proxy.ProtoDNSCrypt:
p.ClientProto = querylog.ClientProtoDNSCrypt
default:

View File

@@ -46,7 +46,7 @@ func (l *testStats) Update(e stats.Entry) {
func TestProcessQueryLogsAndStats(t *testing.T) {
testCases := []struct {
name string
proto string
proto proxy.Proto
addr net.Addr
clientID string
wantLogProto querylog.ClientProto
@@ -69,7 +69,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "cli42",
wantLogProto: querylog.ClientProtoDOT,
wantLogProto: querylog.ClientProtoDoT,
wantStatClient: "cli42",
wantCode: resultCodeSuccess,
reason: filtering.NotFilteredNotFound,
@@ -79,7 +79,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
wantLogProto: querylog.ClientProtoDOT,
wantLogProto: querylog.ClientProtoDoT,
wantStatClient: "1.2.3.4",
wantCode: resultCodeSuccess,
reason: filtering.NotFilteredNotFound,
@@ -89,7 +89,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
proto: proxy.ProtoQUIC,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
wantLogProto: querylog.ClientProtoDOQ,
wantLogProto: querylog.ClientProtoDoQ,
wantStatClient: "1.2.3.4",
wantCode: resultCodeSuccess,
reason: filtering.NotFilteredNotFound,
@@ -99,7 +99,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
proto: proxy.ProtoHTTPS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
wantLogProto: querylog.ClientProtoDOH,
wantLogProto: querylog.ClientProtoDoH,
wantStatClient: "1.2.3.4",
wantCode: resultCodeSuccess,
reason: filtering.NotFilteredNotFound,
@@ -156,7 +156,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RParental,
}}
ups, err := upstream.AddressToUpstream("1.1.1.1", upstream.Options{})
ups, err := upstream.AddressToUpstream("1.1.1.1", nil)
require.Nil(t, err)
for _, tc := range testCases {

View File

@@ -344,13 +344,13 @@ var gctx dnsFilterContext
// ResultRule contains information about applied rules.
type ResultRule struct {
// FilterListID is the ID of the rule's filter list.
FilterListID int64 `json:",omitempty"`
// Text is the text of the rule.
Text string `json:",omitempty"`
// IP is the host IP. It is nil unless the rule uses the
// /etc/hosts syntax or the reason is FilteredSafeSearch.
IP net.IP `json:",omitempty"`
// FilterListID is the ID of the rule's filter list.
FilterListID int64 `json:",omitempty"`
}
// Result contains the result of a request check.
@@ -657,26 +657,43 @@ func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
return nil
}
// matchHostProcessAllowList processes the allowlist logic of host
// matching.
func (d *DNSFilter) matchHostProcessAllowList(host string, dnsres urlfilter.DNSResult) (res Result, err error) {
var rule rules.Rule
if dnsres.NetworkRule != nil {
rule = dnsres.NetworkRule
} else if len(dnsres.HostRulesV4) > 0 {
rule = dnsres.HostRulesV4[0]
} else if len(dnsres.HostRulesV6) > 0 {
rule = dnsres.HostRulesV6[0]
// hostRules is a helper that converts a slice of host rules into a slice of the
// rules.Rule interface values.
func hostRulesToRules(netRules []*rules.HostRule) (res []rules.Rule) {
if netRules == nil {
return nil
}
if rule == nil {
res = make([]rules.Rule, len(netRules))
for i, nr := range netRules {
res[i] = nr
}
return res
}
// matchHostProcessAllowList processes the allowlist logic of host
// matching.
func (d *DNSFilter) matchHostProcessAllowList(
host string,
dnsres urlfilter.DNSResult,
) (res Result, err error) {
var matchedRules []rules.Rule
if dnsres.NetworkRule != nil {
matchedRules = []rules.Rule{dnsres.NetworkRule}
} else if len(dnsres.HostRulesV4) > 0 {
matchedRules = hostRulesToRules(dnsres.HostRulesV4)
} else if len(dnsres.HostRulesV6) > 0 {
matchedRules = hostRulesToRules(dnsres.HostRulesV6)
}
if len(matchedRules) == 0 {
return Result{}, fmt.Errorf("invalid dns result: rules are empty")
}
log.Debug("Filtering: found allowlist rule for host %q: %q list_id: %d",
host, rule.Text(), rule.GetFilterListID())
log.Debug("filtering: allowlist rules for host %q: %+v", host, matchedRules)
return makeResult(rule, NotFilteredAllowList), nil
return makeResult(matchedRules, NotFilteredAllowList), nil
}
// matchHostProcessDNSResult processes the matched DNS filtering result.
@@ -690,21 +707,23 @@ func (d *DNSFilter) matchHostProcessDNSResult(
reason = NotFilteredAllowList
}
return makeResult(dnsres.NetworkRule, reason)
return makeResult([]rules.Rule{dnsres.NetworkRule}, reason)
}
if qtype == dns.TypeA && dnsres.HostRulesV4 != nil {
rule := dnsres.HostRulesV4[0]
res = makeResult(rule, FilteredBlockList)
res.Rules[0].IP = rule.IP.To4()
res = makeResult(hostRulesToRules(dnsres.HostRulesV4), FilteredBlockList)
for i, hr := range dnsres.HostRulesV4 {
res.Rules[i].IP = hr.IP.To4()
}
return res
}
if qtype == dns.TypeAAAA && dnsres.HostRulesV6 != nil {
rule := dnsres.HostRulesV6[0]
res = makeResult(rule, FilteredBlockList)
res.Rules[0].IP = rule.IP.To16()
res = makeResult(hostRulesToRules(dnsres.HostRulesV6), FilteredBlockList)
for i, hr := range dnsres.HostRulesV6 {
res.Rules[i].IP = hr.IP.To16()
}
return res
}
@@ -712,17 +731,14 @@ func (d *DNSFilter) matchHostProcessDNSResult(
if dnsres.HostRulesV4 != nil || dnsres.HostRulesV6 != nil {
// Question type doesn't match the host rules. Return the first
// matched host rule, but without an IP address.
var rule rules.Rule
var matchedRules []rules.Rule
if dnsres.HostRulesV4 != nil {
rule = dnsres.HostRulesV4[0]
matchedRules = []rules.Rule{dnsres.HostRulesV4[0]}
} else if dnsres.HostRulesV6 != nil {
rule = dnsres.HostRulesV6[0]
matchedRules = []rules.Rule{dnsres.HostRulesV6[0]}
}
res = makeResult(rule, FilteredBlockList)
res.Rules[0].IP = net.IP{}
return res
return makeResult(matchedRules, FilteredBlockList)
}
return Result{}
@@ -780,8 +796,7 @@ func (d *DNSFilter) matchHost(
}
res = d.matchHostProcessDNSResult(qtype, dnsres)
if len(res.Rules) > 0 {
r := res.Rules[0]
for _, r := range res.Rules {
log.Debug(
"filtering: found rule %q for host %q, filter list id: %d",
r.Text,
@@ -794,20 +809,20 @@ func (d *DNSFilter) matchHost(
}
// makeResult returns a properly constructed Result.
func makeResult(rule rules.Rule, reason Reason) Result {
res := Result{
Reason: reason,
Rules: []*ResultRule{{
FilterListID: int64(rule.GetFilterListID()),
Text: rule.Text(),
}},
func makeResult(matchedRules []rules.Rule, reason Reason) (res Result) {
resRules := make([]*ResultRule, len(matchedRules))
for i, mr := range matchedRules {
resRules[i] = &ResultRule{
FilterListID: int64(mr.GetFilterListID()),
Text: mr.Text(),
}
}
if reason == FilteredBlockList {
res.IsFiltered = true
return Result{
IsFiltered: reason == FilteredBlockList,
Reason: reason,
Rules: resRules,
}
return res
}
// InitModule manually initializes blocked services map.

View File

@@ -60,29 +60,33 @@ func (d *DNSFilter) checkMatch(t *testing.T, hostname string) {
t.Helper()
res, err := d.CheckHost(hostname, dns.TypeA, &setts)
require.Nilf(t, err, "Error while matching host %s: %s", hostname, err)
assert.Truef(t, res.IsFiltered, "Expected hostname %s to match", hostname)
require.NoErrorf(t, err, "host %q", hostname)
assert.Truef(t, res.IsFiltered, "host %q", hostname)
}
func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) {
t.Helper()
res, err := d.CheckHost(hostname, qtype, &setts)
require.Nilf(t, err, "Error while matching host %s: %s", hostname, err)
assert.Truef(t, res.IsFiltered, "Expected hostname %s to match", hostname)
require.NoErrorf(t, err, "host %q", hostname, err)
require.NotEmpty(t, res.Rules, "host %q", hostname)
assert.Truef(t, res.IsFiltered, "host %q", hostname)
require.NotEmpty(t, res.Rules, "Expected result to have rules")
r := res.Rules[0]
require.NotNilf(t, r.IP, "Expected ip %s to match, actual: %v", ip, r.IP)
assert.Equalf(t, ip, r.IP.String(), "Expected ip %s to match, actual: %v", ip, r.IP)
assert.Equalf(t, ip, r.IP.String(), "host %q", hostname)
}
func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string) {
t.Helper()
res, err := d.CheckHost(hostname, dns.TypeA, &setts)
require.Nilf(t, err, "Error while matching host %s: %s", hostname, err)
assert.Falsef(t, res.IsFiltered, "Expected hostname %s to not match", hostname)
require.NoErrorf(t, err, "host %q", hostname)
assert.Falsef(t, res.IsFiltered, "host %q", hostname)
}
func TestEtcHostsMatching(t *testing.T) {
@@ -112,10 +116,12 @@ func TestEtcHostsMatching(t *testing.T) {
// Empty IPv6.
res, err := d.CheckHost("block.com", dns.TypeAAAA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, "0.0.0.0 block.com", res.Rules[0].Text)
assert.Empty(t, res.Rules[0].IP)
@@ -124,27 +130,34 @@ func TestEtcHostsMatching(t *testing.T) {
// Empty IPv4.
res, err = d.CheckHost("ipv6.com", dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, "::1 ipv6.com", res.Rules[0].Text)
assert.Empty(t, res.Rules[0].IP)
// Two IPv4, the first one returned.
// Two IPv4, both must be returned.
res, err = d.CheckHost("host2", dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
require.Len(t, res.Rules, 2)
assert.Equal(t, res.Rules[0].IP, net.IP{0, 0, 0, 1})
assert.Equal(t, res.Rules[1].IP, net.IP{0, 0, 0, 2})
// One IPv6 address.
res, err = d.CheckHost("host2", dns.TypeAAAA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, res.Rules[0].IP, net.IPv6loopback)
}
@@ -205,8 +218,9 @@ func TestSafeSearch(t *testing.T) {
d := newForTest(&Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
val, ok := d.SafeSearchDomain("www.google.com")
require.True(t, ok, "Expected safesearch to find result for www.google.com")
assert.Equal(t, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
require.True(t, ok)
assert.Equal(t, "forcesafesearch.google.com", val)
}
func TestCheckHostSafeSearchYandex(t *testing.T) {
@@ -226,10 +240,12 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
} {
t.Run(strings.ToLower(host), func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, yandexIP, res.Rules[0].IP)
})
}
@@ -257,9 +273,12 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
} {
t.Run(host, func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, ip, res.Rules[0].IP)
})
}
@@ -272,8 +291,10 @@ func TestSafeSearchCacheYandex(t *testing.T) {
// Check host with disabled safesearch.
res, err := d.CheckHost(domain, dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
require.Empty(t, res.Rules)
yandexIP := net.IPv4(213, 180, 193, 56)
@@ -282,7 +303,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
t.Cleanup(d.Close)
res, err = d.CheckHost(domain, dns.TypeA, &setts)
require.Nilf(t, err, "CheckHost for safesearh domain %s failed cause %s", domain, err)
require.NoError(t, err)
// For yandex we already know valid IP.
require.Len(t, res.Rules, 1)
@@ -292,6 +313,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
}
@@ -304,8 +326,10 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
const domain = "www.google.ru"
res, err := d.CheckHost(domain, dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
require.Empty(t, res.Rules)
d = newForTest(&Config{SafeSearchEnabled: true}, nil)
@@ -314,10 +338,10 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
// Lookup for safesearch domain.
safeDomain, ok := d.SafeSearchDomain(domain)
require.Truef(t, ok, "Failed to get safesearch domain for %s", domain)
require.True(t, ok)
ips, err := resolver.LookupIP(context.Background(), "ip", safeDomain)
require.Nilf(t, err, "Failed to lookup for %s", safeDomain)
require.NoError(t, err)
var ip net.IP
for _, foundIP := range ips {
@@ -329,14 +353,16 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
}
res, err = d.CheckHost(domain, dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
require.Len(t, res.Rules, 1)
assert.True(t, res.Rules[0].IP.Equal(ip))
// Check cache.
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.True(t, cachedValue.Rules[0].IP.Equal(ip))
}
@@ -357,6 +383,7 @@ func TestParentalControl(t *testing.T) {
d.checkMatch(t, matching)
require.Contains(t, logOutput.String(), "Parental lookup for "+matching)
d.checkMatch(t, "www."+matching)
d.checkMatchEmpty(t, "www.yandex.ru")
d.checkMatchEmpty(t, "yandex.ru")
@@ -654,7 +681,8 @@ func TestMatching(t *testing.T) {
t.Cleanup(d.Close)
res, err := d.CheckHost(tc.host, tc.wantDNSType, &setts)
require.Nilf(t, err, "Error while matching host %s: %s", tc.host, err)
require.NoError(t, err)
assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered)
assert.Equalf(t, tc.wantReason, res.Reason, "Hostname %s has wrong reason (%v must be %v)", tc.host, res.Reason, tc.wantReason)
})
@@ -677,23 +705,31 @@ func TestWhitelist(t *testing.T) {
}}
d := newForTest(nil, filters)
require.Nil(t, d.SetFilters(filters, whiteFilters, false))
err := d.SetFilters(filters, whiteFilters, false)
require.NoError(t, err)
t.Cleanup(d.Close)
// Matched by white filter.
res, err := d.CheckHost("host1", dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
assert.Equal(t, res.Reason, NotFilteredAllowList)
require.Len(t, res.Rules, 1)
assert.Equal(t, "||host1^", res.Rules[0].Text)
// Not matched by white filter, but matched by block filter.
res, err = d.CheckHost("host2", dns.TypeA, &setts)
require.Nil(t, err)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
assert.Equal(t, res.Reason, FilteredBlockList)
require.Len(t, res.Rules, 1)
assert.Equal(t, "||host2^", res.Rules[0].Text)
}
@@ -796,7 +832,8 @@ func BenchmarkSafeBrowsing(b *testing.B) {
})
for n := 0; n < b.N; n++ {
res, err := d.CheckHost(blocked, dns.TypeA, &setts)
require.Nilf(b, err, "Error while matching host %s: %s", blocked, err)
require.NoError(b, err)
assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked)
}
}
@@ -812,7 +849,8 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
res, err := d.CheckHost(blocked, dns.TypeA, &setts)
require.Nilf(b, err, "Error while matching host %s: %s", blocked, err)
require.NoError(b, err)
assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked)
}
})
@@ -823,7 +861,8 @@ func BenchmarkSafeSearch(b *testing.B) {
b.Cleanup(d.Close)
for n := 0; n < b.N; n++ {
val, ok := d.SafeSearchDomain("www.google.com")
require.True(b, ok, "Expected safesearch to find result for www.google.com")
require.True(b, ok)
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
}
}
@@ -834,7 +873,8 @@ func BenchmarkSafeSearchParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
val, ok := d.SafeSearchDomain("www.google.com")
require.True(b, ok, "Expected safesearch to find result for www.google.com")
require.True(b, ok)
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
}
})

View File

@@ -49,7 +49,7 @@ func (d *DNSFilter) initSecurityServices() error {
var err error
d.safeBrowsingServer = defaultSafebrowsingServer
d.parentalServer = defaultParentalServer
opts := upstream.Options{
opts := &upstream.Options{
Timeout: dnsTimeout,
ServerIPAddrs: []net.IP{
{94, 140, 14, 15},

View File

@@ -63,13 +63,13 @@ const (
// RuntimeClient information
type RuntimeClient struct {
WhoisInfo *RuntimeClientWhoisInfo
WHOISInfo *RuntimeClientWHOISInfo
Host string
Source clientSource
}
// RuntimeClientWhoisInfo is the filtered WHOIS data for a runtime client.
type RuntimeClientWhoisInfo struct {
// RuntimeClientWHOISInfo is the filtered WHOIS data for a runtime client.
type RuntimeClientWHOISInfo struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
@@ -78,10 +78,13 @@ type RuntimeClientWhoisInfo struct {
type clientsContainer struct {
// TODO(a.garipov): Perhaps use a number of separate indices for
// different types (string, net.IP, and so on).
list map[string]*Client // name -> client
idIndex map[string]*Client // ID -> client
ipToRC map[string]*RuntimeClient // IP -> runtime client
lock sync.Mutex
list map[string]*Client // name -> client
idIndex map[string]*Client // ID -> client
// ipToRC is the IP address to *RuntimeClient map.
ipToRC *aghnet.IPMap
lock sync.Mutex
allTags *aghstrings.Set
@@ -109,7 +112,7 @@ func (clients *clientsContainer) Init(
}
clients.list = make(map[string]*Client)
clients.idIndex = make(map[string]*Client)
clients.ipToRC = make(map[string]*RuntimeClient)
clients.ipToRC = aghnet.NewIPMap(0)
clients.allTags = aghstrings.NewSet(clientTags...)
@@ -250,18 +253,17 @@ func (clients *clientsContainer) onHostsChanged() {
clients.addFromHostsFile()
}
// Exists checks if client with this ID already exists.
func (clients *clientsContainer) Exists(id string, source clientSource) (ok bool) {
// Exists checks if client with this IP address already exists.
func (clients *clientsContainer) Exists(ip net.IP, source clientSource) (ok bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok = clients.findLocked(id)
_, ok = clients.findLocked(ip.String())
if ok {
return true
}
var rc *RuntimeClient
rc, ok = clients.ipToRC[id]
rc, ok := clients.findRuntimeClientLocked(ip)
if !ok {
return false
}
@@ -270,12 +272,12 @@ func (clients *clientsContainer) Exists(id string, source clientSource) (ok bool
return source <= rc.Source
}
func toQueryLogWhois(wi *RuntimeClientWhoisInfo) (cw *querylog.ClientWhois) {
func toQueryLogWHOIS(wi *RuntimeClientWHOISInfo) (cw *querylog.ClientWHOIS) {
if wi == nil {
return &querylog.ClientWhois{}
return &querylog.ClientWHOIS{}
}
return &querylog.ClientWhois{
return &querylog.ClientWHOIS{
City: wi.City,
Country: wi.Country,
Orgname: wi.Orgname,
@@ -287,29 +289,29 @@ func toQueryLogWhois(wi *RuntimeClientWhoisInfo) (cw *querylog.ClientWhois) {
func (clients *clientsContainer) findMultiple(ids []string) (c *querylog.Client, err error) {
for _, id := range ids {
var name string
whois := &querylog.ClientWhois{}
whois := &querylog.ClientWHOIS{}
ip := net.ParseIP(id)
c, ok := clients.Find(id)
if ok {
name = c.Name
} else {
var rc RuntimeClient
rc, ok = clients.FindRuntimeClient(id)
} else if ip != nil {
var rc *RuntimeClient
rc, ok = clients.FindRuntimeClient(ip)
if !ok {
continue
}
name = rc.Host
whois = toQueryLogWhois(rc.WhoisInfo)
whois = toQueryLogWHOIS(rc.WHOISInfo)
}
ip := net.ParseIP(id)
disallowed, disallowedRule := clients.dnsServer.IsBlockedIP(ip)
disallowed, disallowedRule := clients.dnsServer.IsBlockedClient(ip, id)
return &querylog.Client{
Name: name,
DisallowedRule: disallowedRule,
Whois: whois,
WHOIS: whois,
Disallowed: disallowed,
}, nil
}
@@ -356,10 +358,10 @@ func (clients *clientsContainer) findUpstreams(
return c.upstreamConfig, nil
}
var conf proxy.UpstreamConfig
var conf *proxy.UpstreamConfig
conf, err = proxy.ParseUpstreamsConfig(
upstreams,
upstream.Options{
&upstream.Options{
Bootstrap: config.DNS.BootstrapDNS,
Timeout: config.DNS.UpstreamTimeout.Duration,
},
@@ -368,9 +370,9 @@ func (clients *clientsContainer) findUpstreams(
return nil, err
}
c.upstreamConfig = &conf
c.upstreamConfig = conf
return &conf, nil
return conf, nil
}
// findLocked searches for a client by its ID. For internal use only.
@@ -423,22 +425,35 @@ func (clients *clientsContainer) findLocked(id string) (c *Client, ok bool) {
return nil, false
}
// findRuntimeClientLocked finds a runtime client by their IP address. For
// internal use only.
func (clients *clientsContainer) findRuntimeClientLocked(ip net.IP) (rc *RuntimeClient, ok bool) {
var v interface{}
v, ok = clients.ipToRC.Get(ip)
if !ok {
return nil, false
}
rc, ok = v.(*RuntimeClient)
if !ok {
log.Error("clients: bad type %T in ipToRC for %s", v, ip)
return nil, false
}
return rc, true
}
// FindRuntimeClient finds a runtime client by their IP.
func (clients *clientsContainer) FindRuntimeClient(ip string) (RuntimeClient, bool) {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return RuntimeClient{}, false
func (clients *clientsContainer) FindRuntimeClient(ip net.IP) (rc *RuntimeClient, ok bool) {
if ip == nil {
return nil, false
}
clients.lock.Lock()
defer clients.lock.Unlock()
rc, ok := clients.ipToRC[ip]
if ok {
return *rc, true
}
return RuntimeClient{}, false
return clients.findRuntimeClientLocked(ip)
}
// check validates the client.
@@ -620,20 +635,20 @@ func (clients *clientsContainer) Update(name string, c *Client) (err error) {
return nil
}
// SetWhoisInfo sets the WHOIS information for a client.
func (clients *clientsContainer) SetWhoisInfo(ip string, wi *RuntimeClientWhoisInfo) {
// SetWHOISInfo sets the WHOIS information for a client.
func (clients *clientsContainer) SetWHOISInfo(ip net.IP, wi *RuntimeClientWHOISInfo) {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.findLocked(ip)
_, ok := clients.findLocked(ip.String())
if ok {
log.Debug("clients: client for %s is already created, ignore whois info", ip)
return
}
rc, ok := clients.ipToRC[ip]
rc, ok := clients.findRuntimeClientLocked(ip)
if ok {
rc.WhoisInfo = wi
rc.WHOISInfo = wi
log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi)
return
@@ -645,15 +660,16 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, wi *RuntimeClientWhoisI
Source: ClientSourceWHOIS,
}
rc.WhoisInfo = wi
clients.ipToRC[ip] = rc
rc.WHOISInfo = wi
clients.ipToRC.Set(ip, rc)
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
}
// AddHost adds a new IP-hostname pairing. The priorities of the sources is
// taken into account. ok is true if the pairing was added.
func (clients *clientsContainer) AddHost(ip, host string, src clientSource) (ok bool, err error) {
func (clients *clientsContainer) AddHost(ip net.IP, host string, src clientSource) (ok bool, err error) {
clients.lock.Lock()
defer clients.lock.Unlock()
@@ -663,9 +679,9 @@ func (clients *clientsContainer) AddHost(ip, host string, src clientSource) (ok
}
// addHostLocked adds a new IP-hostname pairing. For internal use only.
func (clients *clientsContainer) addHostLocked(ip, host string, src clientSource) (ok bool) {
func (clients *clientsContainer) addHostLocked(ip net.IP, host string, src clientSource) (ok bool) {
var rc *RuntimeClient
rc, ok = clients.ipToRC[ip]
rc, ok = clients.findRuntimeClientLocked(ip)
if ok {
if rc.Source > src {
return false
@@ -676,13 +692,13 @@ func (clients *clientsContainer) addHostLocked(ip, host string, src clientSource
rc = &RuntimeClient{
Host: host,
Source: src,
WhoisInfo: &RuntimeClientWhoisInfo{},
WHOISInfo: &RuntimeClientWHOISInfo{},
}
clients.ipToRC[ip] = rc
clients.ipToRC.Set(ip, rc)
}
log.Debug("clients: added %q -> %q [%d]", ip, host, len(clients.ipToRC))
log.Debug("clients: added %s -> %q [%d]", ip, host, clients.ipToRC.Len())
return true
}
@@ -690,12 +706,21 @@ func (clients *clientsContainer) addHostLocked(ip, host string, src clientSource
// rmHostsBySrc removes all entries that match the specified source.
func (clients *clientsContainer) rmHostsBySrc(src clientSource) {
n := 0
for k, v := range clients.ipToRC {
if v.Source == src {
delete(clients.ipToRC, k)
clients.ipToRC.Range(func(ip net.IP, v interface{}) (cont bool) {
rc, ok := v.(*RuntimeClient)
if !ok {
log.Error("clients: bad type %T in ipToRC for %s", v, ip)
return true
}
if rc.Source == src {
clients.ipToRC.Del(ip)
n++
}
}
return true
})
log.Debug("clients: removed %d client aliases", n)
}
@@ -715,16 +740,23 @@ func (clients *clientsContainer) addFromHostsFile() {
clients.rmHostsBySrc(ClientSourceHostsFile)
n := 0
for ip, names := range hosts {
hosts.Range(func(ip net.IP, v interface{}) (cont bool) {
names, ok := v.([]string)
if !ok {
log.Error("dns: bad type %T in ipToRC for %s", v, ip)
}
for _, name := range names {
ok := clients.addHostLocked(ip, name, ClientSourceHostsFile)
ok = clients.addHostLocked(ip, name, ClientSourceHostsFile)
if ok {
n++
}
}
}
log.Debug("Clients: added %d client aliases from system hosts-file", n)
return true
})
log.Debug("clients: added %d client aliases from system hosts-file", n)
}
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
@@ -752,15 +784,16 @@ func (clients *clientsContainer) addFromSystemARP() {
// TODO(a.garipov): Rewrite to use bufio.Scanner.
lines := strings.Split(string(data), "\n")
for _, ln := range lines {
open := strings.Index(ln, " (")
close := strings.Index(ln, ") ")
if open == -1 || close == -1 || open >= close {
lparen := strings.Index(ln, " (")
rparen := strings.Index(ln, ") ")
if lparen == -1 || rparen == -1 || lparen >= rparen {
continue
}
host := ln[:open]
ip := ln[open+2 : close]
if aghnet.ValidateDomainName(host) != nil || net.ParseIP(ip) == nil {
host := ln[:lparen]
ipStr := ln[lparen+2 : rparen]
ip := net.ParseIP(ipStr)
if aghnet.ValidateDomainName(host) != nil || ip == nil {
continue
}
@@ -796,7 +829,7 @@ func (clients *clientsContainer) updateFromDHCP(add bool) {
continue
}
ok := clients.addHostLocked(l.IP.String(), l.Hostname, ClientSourceDHCP)
ok := clients.addHostLocked(l.IP, l.Hostname, ClientSourceDHCP)
if ok {
n++
}

View File

@@ -26,6 +26,7 @@ func TestClients(t *testing.T) {
ok, err := clients.Add(c)
require.NoError(t, err)
assert.True(t, ok)
c = &Client{
@@ -35,23 +36,27 @@ func TestClients(t *testing.T) {
ok, err = clients.Add(c)
require.NoError(t, err)
assert.True(t, ok)
c, ok = clients.Find("1.1.1.1")
require.True(t, ok)
assert.Equal(t, "client1", c.Name)
c, ok = clients.Find("1:2:3::4")
require.True(t, ok)
assert.Equal(t, "client1", c.Name)
c, ok = clients.Find("2.2.2.2")
require.True(t, ok)
assert.Equal(t, "client2", c.Name)
assert.False(t, clients.Exists("1.2.3.4", ClientSourceHostsFile))
assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile))
assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile))
assert.False(t, clients.Exists(net.IP{1, 2, 3, 4}, ClientSourceHostsFile))
assert.True(t, clients.Exists(net.IP{1, 1, 1, 1}, ClientSourceHostsFile))
assert.True(t, clients.Exists(net.IP{2, 2, 2, 2}, ClientSourceHostsFile))
})
t.Run("add_fail_name", func(t *testing.T) {
@@ -101,8 +106,8 @@ func TestClients(t *testing.T) {
})
require.NoError(t, err)
assert.False(t, clients.Exists("1.1.1.1", ClientSourceHostsFile))
assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile))
assert.False(t, clients.Exists(net.IP{1, 1, 1, 1}, ClientSourceHostsFile))
assert.True(t, clients.Exists(net.IP{1, 1, 1, 2}, ClientSourceHostsFile))
err = clients.Update("client1", &Client{
IDs: []string{"1.1.1.2"},
@@ -113,21 +118,25 @@ func TestClients(t *testing.T) {
c, ok := clients.Find("1.1.1.2")
require.True(t, ok)
assert.Equal(t, "client1-renamed", c.Name)
assert.True(t, c.UseOwnSettings)
nilCli, ok := clients.list["client1"]
require.False(t, ok)
assert.Nil(t, nilCli)
require.Len(t, c.IDs, 1)
assert.Equal(t, "1.1.1.2", c.IDs[0])
})
t.Run("del_success", func(t *testing.T) {
ok := clients.Del("client1-renamed")
require.True(t, ok)
assert.False(t, clients.Exists("1.1.1.2", ClientSourceHostsFile))
assert.False(t, clients.Exists(net.IP{1, 1, 1, 2}, ClientSourceHostsFile))
})
t.Run("del_fail", func(t *testing.T) {
@@ -136,78 +145,93 @@ func TestClients(t *testing.T) {
})
t.Run("addhost_success", func(t *testing.T) {
ok, err := clients.AddHost("1.1.1.1", "host", ClientSourceARP)
ip := net.IP{1, 1, 1, 1}
ok, err := clients.AddHost(ip, "host", ClientSourceARP)
require.NoError(t, err)
assert.True(t, ok)
ok, err = clients.AddHost("1.1.1.1", "host2", ClientSourceARP)
ok, err = clients.AddHost(ip, "host2", ClientSourceARP)
require.NoError(t, err)
assert.True(t, ok)
ok, err = clients.AddHost("1.1.1.1", "host3", ClientSourceHostsFile)
ok, err = clients.AddHost(ip, "host3", ClientSourceHostsFile)
require.NoError(t, err)
assert.True(t, ok)
assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile))
assert.True(t, clients.Exists(ip, ClientSourceHostsFile))
})
t.Run("dhcp_replaces_arp", func(t *testing.T) {
ok, err := clients.AddHost("1.2.3.4", "from_arp", ClientSourceARP)
ip := net.IP{1, 2, 3, 4}
ok, err := clients.AddHost(ip, "from_arp", ClientSourceARP)
require.NoError(t, err)
assert.True(t, ok)
assert.True(t, clients.Exists(ip, ClientSourceARP))
assert.True(t, clients.Exists("1.2.3.4", ClientSourceARP))
ok, err = clients.AddHost("1.2.3.4", "from_dhcp", ClientSourceDHCP)
ok, err = clients.AddHost(ip, "from_dhcp", ClientSourceDHCP)
require.NoError(t, err)
assert.True(t, ok)
assert.True(t, clients.Exists("1.2.3.4", ClientSourceDHCP))
assert.True(t, ok)
assert.True(t, clients.Exists(ip, ClientSourceDHCP))
})
t.Run("addhost_fail", func(t *testing.T) {
ok, err := clients.AddHost("1.1.1.1", "host1", ClientSourceRDNS)
ok, err := clients.AddHost(net.IP{1, 1, 1, 1}, "host1", ClientSourceRDNS)
require.NoError(t, err)
assert.False(t, ok)
})
}
func TestClientsWhois(t *testing.T) {
func TestClientsWHOIS(t *testing.T) {
clients := clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil)
whois := &RuntimeClientWhoisInfo{
whois := &RuntimeClientWHOISInfo{
Country: "AU",
Orgname: "Example Org",
}
t.Run("new_client", func(t *testing.T) {
clients.SetWhoisInfo("1.1.1.255", whois)
ip := net.IP{1, 1, 1, 255}
clients.SetWHOISInfo(ip, whois)
v, _ := clients.ipToRC.Get(ip)
require.NotNil(t, v)
require.NotNil(t, clients.ipToRC["1.1.1.255"])
rc, ok := v.(*RuntimeClient)
require.True(t, ok)
require.NotNil(t, rc)
h := clients.ipToRC["1.1.1.255"]
require.NotNil(t, h)
assert.Equal(t, h.WhoisInfo, whois)
assert.Equal(t, rc.WHOISInfo, whois)
})
t.Run("existing_auto-client", func(t *testing.T) {
ok, err := clients.AddHost("1.1.1.1", "host", ClientSourceRDNS)
ip := net.IP{1, 1, 1, 1}
ok, err := clients.AddHost(ip, "host", ClientSourceRDNS)
require.NoError(t, err)
assert.True(t, ok)
clients.SetWhoisInfo("1.1.1.1", whois)
clients.SetWHOISInfo(ip, whois)
v, _ := clients.ipToRC.Get(ip)
require.NotNil(t, v)
require.NotNil(t, clients.ipToRC["1.1.1.1"])
h := clients.ipToRC["1.1.1.1"]
require.NotNil(t, h)
rc, ok := v.(*RuntimeClient)
require.True(t, ok)
require.NotNil(t, rc)
assert.Equal(t, h.WhoisInfo, whois)
assert.Equal(t, rc.WHOISInfo, whois)
})
t.Run("can't_set_manually-added", func(t *testing.T) {
ip := net.IP{1, 1, 1, 2}
ok, err := clients.Add(&Client{
IDs: []string{"1.1.1.2"},
Name: "client1",
@@ -215,8 +239,10 @@ func TestClientsWhois(t *testing.T) {
require.NoError(t, err)
assert.True(t, ok)
clients.SetWhoisInfo("1.1.1.2", whois)
require.Nil(t, clients.ipToRC["1.1.1.2"])
clients.SetWHOISInfo(ip, whois)
v, _ := clients.ipToRC.Get(ip)
require.Nil(t, v)
assert.True(t, clients.Del("client1"))
})
}
@@ -228,22 +254,26 @@ func TestClientsAddExisting(t *testing.T) {
clients.Init(nil, nil, nil)
t.Run("simple", func(t *testing.T) {
ip := net.IP{1, 1, 1, 1}
// Add a client.
ok, err := clients.Add(&Client{
IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa", "2.2.2.0/24"},
IDs: []string{ip.String(), "1:2:3::4", "aa:aa:aa:aa:aa:aa", "2.2.2.0/24"},
Name: "client1",
})
require.NoError(t, err)
assert.True(t, ok)
// Now add an auto-client with the same IP.
ok, err = clients.AddHost("1.1.1.1", "test", ClientSourceRDNS)
ok, err = clients.AddHost(ip, "test", ClientSourceRDNS)
require.NoError(t, err)
assert.True(t, ok)
})
t.Run("complicated", func(t *testing.T) {
testIP := net.IP{1, 2, 3, 4}
var err error
ip := net.IP{1, 2, 3, 4}
// First, init a DHCP server with a single static lease.
config := dhcpd.ServerConfig{
@@ -258,12 +288,14 @@ func TestClientsAddExisting(t *testing.T) {
},
}
clients.dhcpServer = dhcpd.Create(config)
clients.dhcpServer, err = dhcpd.Create(config)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove("leases.db") })
err := clients.dhcpServer.AddStaticLease(dhcpd.Lease{
err = clients.dhcpServer.AddStaticLease(&dhcpd.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: testIP,
IP: ip,
Hostname: "testhost",
Expiry: time.Now().Add(time.Hour),
})
@@ -271,7 +303,7 @@ func TestClientsAddExisting(t *testing.T) {
// Add a new client with the same IP as for a client with MAC.
ok, err := clients.Add(&Client{
IDs: []string{testIP.String()},
IDs: []string{ip.String()},
Name: "client2",
})
require.NoError(t, err)

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"net"
"net/http"
"github.com/AdguardTeam/golibs/log"
)
// clientJSON is a common structure used by several handlers to deal with
@@ -24,7 +26,7 @@ type clientJSON struct {
// the allowlist.
DisallowedRule *string `json:"disallowed_rule,omitempty"`
WhoisInfo *RuntimeClientWhoisInfo `json:"whois_info,omitempty"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
Name string `json:"name"`
@@ -42,15 +44,15 @@ type clientJSON struct {
}
type runtimeClientJSON struct {
WhoisInfo *RuntimeClientWhoisInfo `json:"whois_info"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
IP string `json:"ip"`
Name string `json:"name"`
Source string `json:"source"`
IP net.IP `json:"ip"`
}
type clientListJSON struct {
Clients []clientJSON `json:"clients"`
Clients []*clientJSON `json:"clients"`
RuntimeClients []runtimeClientJSON `json:"auto_clients"`
Tags []string `json:"supported_tags"`
}
@@ -66,11 +68,20 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, _ *http
cj := clientToJSON(c)
data.Clients = append(data.Clients, cj)
}
for ip, rc := range clients.ipToRC {
clients.ipToRC.Range(func(ip net.IP, v interface{}) (cont bool) {
rc, ok := v.(*RuntimeClient)
if !ok {
log.Error("dns: bad type %T in ipToRC for %s", v, ip)
return true
}
cj := runtimeClientJSON{
IP: ip,
Name: rc.Host,
WhoisInfo: rc.WhoisInfo,
WHOISInfo: rc.WHOISInfo,
Name: rc.Host,
IP: ip,
}
cj.Source = "etc/hosts"
@@ -86,7 +97,9 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, _ *http
}
data.RuntimeClients = append(data.RuntimeClients, cj)
}
return true
})
data.Tags = clientTags
@@ -118,8 +131,8 @@ func jsonToClient(cj clientJSON) (c *Client) {
}
// Convert Client object to JSON
func clientToJSON(c *Client) clientJSON {
cj := clientJSON{
func clientToJSON(c *Client) (cj *clientJSON) {
return &clientJSON{
Name: c.Name,
IDs: c.IDs,
Tags: c.Tags,
@@ -134,19 +147,6 @@ func clientToJSON(c *Client) clientJSON {
Upstreams: c.Upstreams,
}
return cj
}
// runtimeClientToJSON converts a RuntimeClient into a JSON struct.
func runtimeClientToJSON(ip string, rc RuntimeClient) (cj clientJSON) {
cj = clientJSON{
Name: rc.Host,
IDs: []string{ip},
WhoisInfo: rc.WhoisInfo,
}
return cj
}
// Add a new client
@@ -230,7 +230,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
// 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]clientJSON{}
data := []map[string]*clientJSON{}
for i := 0; i < len(q); i++ {
idStr := q.Get(fmt.Sprintf("ip%d", i))
if idStr == "" {
@@ -239,20 +239,16 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
ip := net.ParseIP(idStr)
c, ok := clients.Find(idStr)
var cj clientJSON
var cj *clientJSON
if !ok {
var found bool
cj, found = clients.findRuntime(ip, idStr)
if !found {
continue
}
cj = clients.findRuntime(ip, idStr)
} else {
cj = clientToJSON(c)
disallowed, rule := clients.dnsServer.IsBlockedIP(ip)
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
}
data = append(data, map[string]clientJSON{
data = append(data, map[string]*clientJSON{
idStr: cj,
})
}
@@ -265,39 +261,37 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
}
// findRuntime looks up the IP in runtime and temporary storages, like
// /etc/hosts tables, DHCP leases, or blocklists.
func (clients *clientsContainer) findRuntime(ip net.IP, idStr string) (cj clientJSON, found bool) {
if ip == nil {
return cj, false
}
rc, ok := clients.FindRuntimeClient(idStr)
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
// non-nil.
func (clients *clientsContainer) findRuntime(ip net.IP, idStr string) (cj *clientJSON) {
rc, ok := clients.FindRuntimeClient(ip)
if !ok {
// It is still possible that the IP used to be in the runtime
// clients list, but then the server was reloaded. So, check
// the DNS server's blocked IP list.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2428.
disallowed, rule := clients.dnsServer.IsBlockedIP(ip)
if rule == "" {
return clientJSON{}, false
}
cj = clientJSON{
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)
cj = &clientJSON{
IDs: []string{idStr},
Disallowed: &disallowed,
DisallowedRule: &rule,
WhoisInfo: &RuntimeClientWhoisInfo{},
WHOISInfo: &RuntimeClientWHOISInfo{},
}
return cj, true
return cj
}
cj = runtimeClientToJSON(idStr, rc)
disallowed, rule := clients.dnsServer.IsBlockedIP(ip)
cj = &clientJSON{
Name: rc.Host,
IDs: []string{idStr},
WHOISInfo: rc.WHOISInfo,
}
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
return cj, true
return cj
}
// RegisterClientsHandlers registers HTTP handlers

View File

@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
@@ -102,11 +103,12 @@ type dnsConfig struct {
// time interval for statistics (in days)
StatsInterval uint32 `yaml:"statistics_interval"`
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file
QueryLogInterval uint32 `yaml:"querylog_interval"` // time interval for query log (in days)
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file
// QueryLogInterval is the interval for query log's files rotation.
QueryLogInterval Duration `yaml:"querylog_interval"`
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
dnsforward.FilteringConfig `yaml:",inline"`
@@ -135,11 +137,11 @@ type dnsConfig struct {
}
type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DoT/DoH/HTTPS) status
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
ForceHTTPS bool `yaml:"force_https" json:"force_https,omitempty"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
PortHTTPS int `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DoT will be disabled
PortDNSOverQUIC int `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"` // DNS-over-QUIC port. If 0, DoQ will be disabled
// PortDNSCrypt is the port for DNSCrypt requests. If it's zero,
@@ -152,8 +154,8 @@ type tlsConfigSettings struct {
// https://github.com/ameshkov/dnscrypt.
DNSCryptConfigFile string `yaml:"dnscrypt_config_file" json:"dnscrypt_config_file"`
// Allow DOH queries via unencrypted HTTP (e.g. for reverse proxying)
AllowUnencryptedDOH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
// Allow DoH queries via unencrypted HTTP (e.g. for reverse proxying)
AllowUnencryptedDoH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
}
@@ -185,7 +187,7 @@ var config = configuration{
},
FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24,
UpstreamTimeout: Duration{dnsforward.DefaultTimeout},
UpstreamTimeout: Duration{Duration: dnsforward.DefaultTimeout},
LocalDomainName: "lan",
ResolveClients: true,
UsePrivateRDNS: true,
@@ -212,7 +214,7 @@ func initConfig() {
config.DNS.QueryLogEnabled = true
config.DNS.QueryLogFileEnabled = true
config.DNS.QueryLogInterval = 90
config.DNS.QueryLogInterval = Duration{Duration: 90 * 24 * time.Hour}
config.DNS.QueryLogMemSize = 1000
config.DNS.CacheSize = 4 * 1024 * 1024
@@ -281,7 +283,7 @@ func parseConfig() error {
}
if config.DNS.UpstreamTimeout.Duration == 0 {
config.DNS.UpstreamTimeout = Duration{dnsforward.DefaultTimeout}
config.DNS.UpstreamTimeout = Duration{Duration: dnsforward.DefaultTimeout}
}
return nil
@@ -328,7 +330,7 @@ func (c *configuration) write() error {
Context.queryLog.WriteDiskConfig(&dc)
config.DNS.QueryLogEnabled = dc.Enabled
config.DNS.QueryLogFileEnabled = dc.FileEnabled
config.DNS.QueryLogInterval = dc.RotationIvl
config.DNS.QueryLogInterval = Duration{Duration: dc.RotationIvl}
config.DNS.QueryLogMemSize = dc.MemSize
config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP
}

View File

@@ -198,9 +198,9 @@ func registerControlHandlers() {
httpRegister(http.MethodPost, "/control/update", handleUpdate)
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
// No auth is necessary for DOH/DOT configurations
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDOH))
Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDOT))
// No auth is necessary for DoH/DoT configurations
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDoT))
RegisterAuthHandlers()
}

View File

@@ -11,7 +11,7 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/updater"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@@ -141,7 +141,7 @@ func (vr *versionResponse) confirmAutoUpdate() {
tlsConf.PortDNSOverQUIC < 1024)) ||
config.BindPort < 1024 ||
config.DNS.Port < 1024) {
canUpdate, _ = aghos.CanBindPrivilegedPorts()
canUpdate, _ = aghnet.CanBindPrivilegedPorts()
}
vr.CanAutoUpdate = &canUpdate
}

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