Compare commits

...

32 Commits

Author SHA1 Message Date
Artem Krisanov
521aedc5bc Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-21485 2023-04-18 14:27:28 +03:00
Ainar Garipov
1842f7d888 Pull request 1831: home: imp depr option doc
Merge in DNS/adguard-home from imp-option-doc to master

Squashed commit of the following:

commit 267410fcc2c9e757c7d8fb7d9059a709932dda9d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Apr 17 14:30:12 2023 +0300

    home: imp depr option doc
2023-04-18 14:18:28 +03:00
Artem Krisanov
40ff26ea21 Login theme bugfix. 2023-04-18 14:11:28 +03:00
Artem Krisanov
4afd39b22f AG-21136 - Added local storage theme key.
Updates#5444

Squashed commit of the following:

commit 7b0b108f41ebb5e98861cdd20029c12d3a3fc5f4
Merge: 38df28db0 e43ba1788
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Mon Apr 17 15:58:15 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into 5444-white-screen

commit 38df28db0739e47d3fb605f648fa493b58709d77
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Fri Apr 14 17:54:00 2023 +0300

    Deleted useless tag.

commit 78ef9d911ccf74b69a9ae5626ea8f31cb9338ae0
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Fri Apr 14 17:53:17 2023 +0300

    Set initial body data-theme.

commit f470b3aa79500edd0726b7ed37e6e5940b6ce3ff
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 16:42:25 2023 +0300

    Revert login changes.

commit 7c4734ed02a670a59d0b9ff04e06bc1d396223a8
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 15:51:24 2023 +0300

    Added setting theme into html.Changed overlay background color to variable.

commit a3743be0e69489489755db8ff55541b9a6281300
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 17:58:47 2023 +0300

    Added local storage theme key.
2023-04-17 16:07:20 +03:00
Artem Krisanov
e43ba17884 AG-21212 - Custom logs and stats retention
Updates#3404

Squashed commit of the following:

commit b68a1d08b0676ebb7abbb13c9274c8d509cd6eed
Merge: 81265147 6d402dc8
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Mon Apr 17 15:48:33 2023 +0300

    Merge master

commit 81265147b5613be11a6621a416f9588c0e1c0ef5
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 10:54:39 2023 +0300

    Changed query log 'retention' --> 'rotation'.

commit 02c5dc0b54bca9ec293ee8629d769489bc5dc533
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 13:22:22 2023 +0300

    Custom inputs for query log and stats configs.

commit 21dbfbd8aac868baeea0f8b25d14786aecf09a0d
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 18:12:40 2023 +0300

    Temporary changes.
2023-04-17 15:57:57 +03:00
Eugene Burkov
6d402dc86c Pull request 1830: 5712-rollback-dhcp
Merge in DNS/adguard-home from 5712-rollback-dhcp to master

Updates #5712.

Squashed commit of the following:

commit 3d53a6385ad08dfad0b7ac28bb057cf25608554d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:30:18 2023 +0300

    dhcpd: imp import

commit 86bd55b0225b5d9067bd0bf9e6def1e52dd27124
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:26:41 2023 +0300

    all: return todo

commit 629c548989a464a9cf461fffc0815b99a00c4851
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:24:10 2023 +0300

    all: log changes

commit e4c369e55cbcc7c73d73d8df333996862e1e146a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:03:03 2023 +0300

    dhcpd: revert raw for darwin
2023-04-14 16:58:07 +03:00
Stanislav Chzhen
18acdf9b09 Pull request 1809: 4299-querylog-stats-clients-api
Merge in DNS/adguard-home from 4299-querylog-stats-clients-api to master

Squashed commit of the following:

commit 066100a7869d7572c4ae65b3c7b1487ac50baf15
Merge: 95bc00c0 5da77514
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 14 13:57:30 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients-api

commit 95bc00c0b3d05b262ee0b90be9757e61cac0778c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:48:39 2023 +0300

    all: fix typo

commit 4b868da48f0c976d204346e40ba948803be6397f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:42:52 2023 +0300

    all: fix text label

commit 7a3ba5c7f688bd53cf761b5e8e614fbe251bd006
Merge: 315256e3 6c8d89a4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:34:59 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients-api

commit 315256e3f3861b5116962f7c47384b7c72e41813
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 11 19:07:18 2023 +0300

    all: ignore search, unit

commit 28c6ffec9558e7c38d7bd12055eabddb8f5675c2
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 15:08:35 2023 +0300

    Added 'Protection' and 'Query Log and statistics' sections to client settings. Added checkboxes to ignore client in (query log/statistics)

commit 2657bd2b820d8b2b3d71d23e4545c867b9ae6cdf
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 17:28:59 2023 +0300

    all: add todo

commit e151fcbc0c36d8e6a5c091fbf374bf0e35804699
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 15:15:46 2023 +0300

    openapi: imp docs

commit 31875cbbd1bd09a73baa3636d0cc242b5ac35059
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 13:02:31 2023 +0300

    all: add querylog stats client ignore api
2023-04-14 15:25:04 +03:00
Ainar Garipov
5da7751463 Pull request 1829: 5725-querylog-orig-ans
Closes #5725.

Squashed commit of the following:

commit a9e5fc47fc0a752f427e006ab1c59e260239ee5a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 20:25:12 2023 +0300

    querylog: fix orig ans assignment
2023-04-13 20:51:57 +03:00
Ainar Garipov
7631ca4ab3 Pull request 1828: AG-21377-default-safe-search
Merge in DNS/adguard-home from AG-21377-default-safe-search to master

Squashed commit of the following:

commit 35c66b97c787d02fe6f2ffb6902dcd9b6f9b9569
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 20:05:13 2023 +0300

    home: fix default safe search svcs
2023-04-13 20:17:59 +03:00
Ainar Garipov
c6d4f2317e Pull request 1827: upd-deps
Merge in DNS/adguard-home from upd-deps to master

Squashed commit of the following:

commit 4892dc4ed6df76d8733e6799744b095c5db1db6c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 18:22:10 2023 +0300

    all: upd dnscrypt, skel
2023-04-13 19:13:11 +03:00
Eugene Burkov
0ea224a9e4 Pull request 1826: 5714 fix-docker-health
Merge in DNS/adguard-home from 5714-fix-docker-health to master

Updates #5714.

Squashed commit of the following:

commit 61251bffd7a21f1ceb867cc89de0a171645ca4c2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Apr 13 16:45:41 2023 +0300

    docker: use localhost for unspecified
2023-04-13 17:40:45 +03:00
Ainar Garipov
d78a3edb22 Pull request 1825: 5721-dnscrypt-panic
Updates #5721.

Squashed commit of the following:

commit edf7801e2028aa31d59440158d3fcf2ef95d7013
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 15:44:18 2023 +0300

    all: fix dnscrypt panic
2023-04-13 15:50:01 +03:00
Stanislav Chzhen
bbbdea2635 Pull request 1824: fix-chlog
Merge in DNS/adguard-home from fix-chlog to master

Squashed commit of the following:

commit 98e8f5e1436f6c2049f002a4c2211dd2a2e9920c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 12:39:11 2023 +0300

    all: fix chlog more

commit deef876541877bc7773e596b39f40c3341ae903a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:57:10 2023 +0300

    all: fix chlog
2023-04-13 13:07:50 +03:00
Artem Krisanov
6c8d89a4da AG-20835 - Deleted unused methods and variable.
Squashed commit of the following:

commit 3d633703fc60e42d26ccf3b5697370c49ffa1a82
Merge: 09119e2e f082312e
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 10:58:31 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-20835

commit 09119e2ec6f3116986d3ddeb48ee2eb18c1cd9d7
Merge: 085bbb5c 67d8b7df
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 14:55:22 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-20835

commit 085bbb5cabb14d5388e45ab2022840d44ae42874
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 14:12:28 2023 +0300

    Deleted unused methods and variable.
2023-04-13 11:07:13 +03:00
Ainar Garipov
f082312e49 Pull request 1822: upd-deps
Merge in DNS/adguard-home from upd-deps to master

Squashed commit of the following:

commit 7ce7532d4a914c678557a6cd9e31c707da8a1a66
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 12 17:14:58 2023 +0300

    all: upd deps
2023-04-12 17:20:29 +03:00
Ainar Garipov
ea03d1af93 Pull request 1821: all: upd chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit a1a550ec26227c2050eb192e1ae53577d699e0ea
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 12 16:50:53 2023 +0300

    all: upd chlog
2023-04-12 16:56:33 +03:00
Ainar Garipov
67d8b7df90 Pull request 1820: upd-all
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit 88aee29c6685816eaef8ff63af380d609f308ead
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 12 14:25:19 2023 +0300

    all: upd i18n, svcs
2023-04-12 14:29:56 +03:00
Ainar Garipov
b23ea0a690 Pull request 1816: 5704-rm-endian
Updates #5704.

Squashed commit of the following:

commit 927faf8c3ae0a5deea651ea4249a90ffc80a21c9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 11 20:02:52 2023 +0300

    all: rm our copy of endian
2023-04-11 20:07:39 +03:00
Ainar Garipov
a186b5c436 Pull request 1815: 5701-log-ip
Updates #5701.

Squashed commit of the following:

commit 332530cbae602e9b0e4c89351bde6b0da017fc67
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 11 18:56:27 2023 +0300

    home: imp docs

commit 35a649ffed9ca736e63842f077411c5f5cbb57f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 11 18:27:25 2023 +0300

    home: fix login attempt logging
2023-04-11 19:43:38 +03:00
Artem Krisanov
6da7392345 AG-20897 - Fixed bug with checkbox's actual state on filter adding.
Updates#5647

Squashed commit of the following:

commit 610f4dae6f7e0e2576669489030eb43bd06d6da6
Merge: fb0c0472 c1924a8b
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 18:16:44 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into 5647-filter-adding

commit fb0c04725c41707ca6a1e4ce36a8e17a6898ee27
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Mon Apr 10 16:51:45 2023 +0300

    Fixed bug with checkbox's actual state on filter adding.
2023-04-11 18:39:46 +03:00
Artem Krisanov
c1924a8b8a AG-21184 - Added transforming protection_disabled_duration field value from 0 to null;
Updates#5689

Squashed commit of the following:

commit 4de24101622c8c76be8e2f1eb1b670c36a1e74c8
Merge: fd9ee1fb 0376afb3
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 18:12:57 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into 5689-protection-status

commit fd9ee1fba4e507875dd8b2b605d14cd78aa4e918
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 14:10:44 2023 +0300

    Added transforming protection_disabled_duration field value from 0 to null;
2023-04-11 18:16:24 +03:00
Ainar Garipov
0376afb38e Pull request 1814: AG-21291-web-races
Merge in DNS/adguard-home from AG-21291-web-races to master

Squashed commit of the following:

commit 1134013f928aa5e186db3b6d0450e425cb053e9c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 11 16:52:52 2023 +0300

    home: fix web api races
2023-04-11 17:22:51 +03:00
Stanislav Chzhen
230d7b8c17 Pull request 1813: fix-chlog
Merge in DNS/adguard-home from fix-chlog to master

Squashed commit of the following:

commit 47f6b33780d7c1d99a6fc36ddf435db068938174
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 11 16:38:04 2023 +0300

    all: fix chlog
2023-04-11 16:43:47 +03:00
Ainar Garipov
950ecb1f5e Pull request 1812: AG-21286
Merge in DNS/adguard-home from AG-21286 to master

Squashed commit of the following:

commit 587b4a3704fd63aa3da6c1be83f8a49bf4e27b00
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 11 14:15:05 2023 +0300

    all: fix negative pause duration
2023-04-11 15:02:29 +03:00
Ildar Kamalov
9e14d5f99f Pull request: fix missing icons on login page
Updates #5620

Squashed commit of the following:

commit 61969c83c3dd6bd6688f0aabc9d6160b53701866
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Apr 10 14:50:47 2023 +0300

    AG-20691 fix theme select on login page

commit c87b6c37284021f33f440dcd31be5b653e8e689d
Merge: aa744756 89bf3721
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Apr 10 14:21:01 2023 +0300

    Merge branch 'master' into AG-20691

commit aa744756d18d9ed3bc7f60108235d8403e7cb5e0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Apr 7 15:53:38 2023 +0300

    AG-20691 fix missing icons on login page
2023-04-10 17:12:52 +03:00
Stanislav Chzhen
89bf3721b5 Pull request 1800: AG-20352-dhcpd-lease-is-static
Merge in DNS/adguard-home from AG-20352-dhcpd-lease-is-static to master

Squashed commit of the following:

commit e4f4278a7ffa0f084ed41472dd3e7de4466c9f50
Merge: b6e3b62e 15bba281
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 11:13:05 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-is-static

commit b6e3b62ed4e7bc17fc3fdd2f4faa940a7f4334c2
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 6 15:20:42 2023 +0300

    dhcpd: imp naming

commit e2d9ed0832b329f4cebcf8cbfcfadc5755fe441b
Merge: ecd244a6 10bffd89
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 5 16:53:55 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-is-static

commit ecd244a60841d3cb96d292da688e353650baf645
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 5 16:53:15 2023 +0300

    dhcpd: add lease json form

commit 9ebc246ed4711ff46091326a7cb35ea90b880cff
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 5 13:11:25 2023 +0300

    dhcpd: imp code

commit fc3d0cdaebf9e32e73d57e80296f9891896259cf
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 5 13:00:59 2023 +0300

    all: fix json

commit d722578543b98b1fefabecd6486f3bc102263d71
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 5 12:23:34 2023 +0300

    dhcpd: add lease is static
2023-04-10 11:17:05 +03:00
Ainar Garipov
15bba281ee Pull request 1807: upd-golibs
Merge in DNS/adguard-home from upd-golibs to master

Squashed commit of the following:

commit cde42a72c2140245f345681cbb936ed3bc4645a1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Apr 7 13:57:02 2023 +0300

    all: upd golibs, use hdrs
2023-04-07 14:21:37 +03:00
Stanislav Chzhen
f9fe3172c4 Pull request 1791: 4299-querylog-stats-clients
Merge in DNS/adguard-home from 4299-querylog-stats-clients to master

Squashed commit of the following:

commit 33b80b67224f7c1a15bee8e6a23d9d5bab6ac629
Merge: 61964fdd 5d5a7295
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 7 12:43:22 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients

commit 61964fdd02221abbddedf2d6d02bb0bce6845362
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 7 12:42:01 2023 +0300

    dnsforward: imp code

commit 7382168500bab6ca7494d39aabfc2d7bfceb5d24
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 7 11:13:07 2023 +0300

    all: imp code, chlog

commit c7852902f635af6c296dcb6735f7b0bfb83f4e87
Merge: aa4dc0a5 a55cbbe7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 6 14:34:24 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients

commit aa4dc0a54e95bc5b24718ec158340b631a822801
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 6 12:54:02 2023 +0300

    all: imp code

commit dd541f0cd7ecbf0afcf10ccbd130fd1d1fa4c1c4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 31 13:01:53 2023 +0300

    querylog: fix typo

commit d2c8fdb35b04d27c8957fa027882fde704cc07be
Merge: 83d0baa1 2eb3bf6e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 31 12:36:49 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients

commit 83d0baa1f1202f9c62d4be2041d7aed12ee9ab2c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 31 12:35:15 2023 +0300

    all: add tests

commit a459f19f25cf9646d145813fe7834b2d9979c516
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 29 16:51:53 2023 +0300

    all: add clients querylog stats ignore
2023-04-07 13:17:40 +03:00
Ainar Garipov
5d5a729569 Pull request 1806: 5661-fix-protection-update-lock
Updates #5661.

Squashed commit of the following:

commit 02e83c75c8f44f084c0cb8d33b7d6a524c8c1b0e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 19:28:17 2023 +0300

    dnsforward: imp logs

commit 0f27265fc94f0e3b8e2dee79dbf9ab5344416c61
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 19:18:19 2023 +0300

    dnsforward: imp locks
2023-04-06 19:33:25 +03:00
Ainar Garipov
b1120221c7 Pull request 1805: imp-pprof
Merge in DNS/adguard-home from imp-pprof to master

Squashed commit of the following:

commit 2d56e0820fd26720c87978d0096405257100f3f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 15:10:12 2023 +0300

    home: imp rate

commit 97611a26e224f354d42ea16f3193c6cb72de3a48
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 15:03:33 2023 +0300

    home: imp pprof block, mutex profiles
2023-04-06 15:14:30 +03:00
Ainar Garipov
f553eee842 Pull request 1804: newline-lint
Merge in DNS/adguard-home from newline-lint to master

Squashed commit of the following:

commit 2fc0b662b9ac9d954275c5ebe8c140be4cd365be
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 14:16:14 2023 +0300

    client: rm line

commit 10246727179a84094edd17fad5cd6f0a5c38b821
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 13:51:06 2023 +0300

    all: add newline lint
2023-04-06 14:21:46 +03:00
Ainar Garipov
61b4043775 Pull request 1803: 5685-fix-safe-search
Updates #5685.

Squashed commit of the following:

commit 5312147abfa0914c896acbf1e88f8c8f1af90f2b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 6 14:09:44 2023 +0300

    safesearch: imp tests, logs

commit 298b5d24ce292c5f83ebe33d1e92329e4b3c1acc
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 5 20:36:16 2023 +0300

    safesearch: fix filters, logging

commit 63d6ca5d694d45705473f2f0410e9e0b49cf7346
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 5 20:24:47 2023 +0300

    all: dry; fix logs

commit fdbf2f364fd0484b47b3161bf6f4581856fdf47b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 5 20:01:08 2023 +0300

    all: fix safe search update
2023-04-06 14:12:50 +03:00
114 changed files with 2488 additions and 905 deletions

View File

@@ -14,17 +14,53 @@ and this project adheres to
<!-- <!--
## [v0.108.0] - TBA ## [v0.108.0] - TBA
## [v0.107.28] - 2023-04-12 (APPROX.) ## [v0.107.29] - 2023-04-26 (APPROX.)
See also the [v0.107.28 GitHub milestone][ms-v0.107.28]. See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
[ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?closed=1 [ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Added ### Added
- The ability to exclude client activity from the query log or statistics by
editing client's settings on the Clients settings page in the UI ([#1717],
[#4299]).
### Fixed
- The `github.com/mdlayher/raw` dependency has been temporarily returned to
support raw connections on Darwin ([#5712]).
- Incorrect recording of blocked results as “Blocked by CNAME or IP” in the
query log ([#5725]).
- All Safe Search services being unchecked by default.
- Panic when a DNSCrypt stamp is invalid ([#5721]).
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721
[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.28] - 2023-04-12
See also the [v0.107.28 GitHub milestone][ms-v0.107.28].
### Added
- The ability to exclude client activity from the query log or statistics by
using the new properties `ignore_querylog` and `ignore_statistics` of the
items of the `clients.persistent` array ([#1717], [#4299]). The UI changes
are coming in the upcoming releases.
- Better profiling information when `debug_pprof` is set to `true`.
- IPv6 support in Safe Search for some services.
- The ability to make bootstrap DNS lookups prefer IPv6 addresses to IPv4 ones - The ability to make bootstrap DNS lookups prefer IPv6 addresses to IPv4 ones
using the new `dns.bootstrap_prefer_ipv6` configuration file property using the new `dns.bootstrap_prefer_ipv6` configuration file property
([#4262]). ([#4262]).
@@ -32,20 +68,20 @@ NOTE: Add new changes BELOW THIS COMMENT.
- The new HTTP API `POST /control/protection`, that updates protection state - The new HTTP API `POST /control/protection`, that updates protection state
and adds an optional pause duration ([#1333]). The format of request body and adds an optional pause duration ([#1333]). The format of request body
is described in `openapi/openapi.yaml`. The duration of this pause could is described in `openapi/openapi.yaml`. The duration of this pause could
also be set with the config field `protection_disabled_until` in `dns` also be set with the property `protection_disabled_until` in the `dns` object
section of the YAML configuration file. of the YAML configuration file.
- The ability to create a static DHCP lease from a dynamic one more easily - The ability to create a static DHCP lease from a dynamic one more easily
([#3459]). ([#3459]).
- Two new HTTP APIs, `PUT /control/stats/config/update` and `GET - Two new HTTP APIs, `PUT /control/stats/config/update` and `GET
control/stats/config`, which can be used to set and receive the query log control/stats/config`, which can be used to set and receive the query log
configuration. See openapi/openapi.yaml for the full description. configuration. See `openapi/openapi.yaml` for the full description.
- Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET - Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET
control/querylog/config`, which can be used to set and receive the statistics control/querylog/config`, which can be used to set and receive the statistics
configuration. See openapi/openapi.yaml for the full description. configuration. See `openapi/openapi.yaml` for the full description.
- The ability to set custom IP for EDNS Client Subnet by using the DNS-server - The ability to set custom IP for EDNS Client Subnet by using the DNS-server
configuration section on the DNS settings page in the UI ([#1472]). configuration section on the DNS settings page in the UI ([#1472]).
- The ability to manage safesearch for each service by using the new - The ability to manage Safe Search for each service by using the new
`safe_search` field ([#1163]). `safe_search` property ([#1163]).
### Changed ### Changed
@@ -74,9 +110,9 @@ In this release, the schema version has changed from 17 to 20.
To rollback this change, convert the property back into days and change the To rollback this change, convert the property back into days and change the
`schema_version` back to `19`. `schema_version` back to `19`.
- The `dns.safesearch_enabled` field has been replaced with `safe_search` - The `dns.safesearch_enabled` property has been replaced with `safe_search`
object containing per-service settings. object containing per-service settings.
- The `clients.persistent.safesearch_enabled` field has been replaced with - The `clients.persistent.safesearch_enabled` property has been replaced with
`safe_search` object containing per-service settings. `safe_search` object containing per-service settings.
```yaml ```yaml
@@ -95,7 +131,7 @@ In this release, the schema version has changed from 17 to 20.
``` ```
To rollback this change, move the value of `dns.safe_search.enabled` into the To rollback this change, move the value of `dns.safe_search.enabled` into the
`dns.safesearch_enabled`, then remove `dns.safe_search` field. Do the same `dns.safesearch_enabled`, then remove `dns.safe_search` property. Do the same
client's specific `clients.persistent.safesearch` and then change the client's specific `clients.persistent.safesearch` and then change the
`schema_version` back to `17`. `schema_version` back to `17`.
@@ -105,7 +141,7 @@ In this release, the schema version has changed from 17 to 20.
`PUT /control/safesearch/settings` API. `PUT /control/safesearch/settings` API.
- The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new - The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new
`PUT /control/safesearch/settings` API `PUT /control/safesearch/settings` API
- The `safesearch_enabled` field is deprecated in the following HTTP APIs: - The `safesearch_enabled` property is deprecated in the following HTTP APIs:
- `GET /control/clients`; - `GET /control/clients`;
- `POST /control/clients/add`; - `POST /control/clients/add`;
- `POST /control/clients/update`; - `POST /control/clients/update`;
@@ -116,30 +152,35 @@ In this release, the schema version has changed from 17 to 20.
/control/stats/config` API instead. /control/stats/config` API instead.
**NOTE:** If interval is custom then it will be equal to `90` days for **NOTE:** If interval is custom then it will be equal to `90` days for
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`. compatibility reasons. See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
- The `POST /control/stats_config` HTTP API; use the new `PUT - The `POST /control/stats_config` HTTP API; use the new `PUT
/control/stats/config/update` API instead. /control/stats/config/update` API instead.
- The `GET /control/querylog_info` HTTP API; use the new `GET - The `GET /control/querylog_info` HTTP API; use the new `GET
/control/querylog/config` API instead. /control/querylog/config` API instead.
**NOTE:** If interval is custom then it will be equal to `90` days for **NOTE:** If interval is custom then it will be equal to `90` days for
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`. compatibility reasons. See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
- The `POST /control/querylog_config` HTTP API; use the new `PUT - The `POST /control/querylog_config` HTTP API; use the new `PUT
/control/querylog/config/update` API instead. /control/querylog/config/update` API instead.
### Fixed
- Logging of the client's IP address after failed login attempts ([#5701]).
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163 [#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333 [#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472 [#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290 [#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459 [#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262 [#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567 [#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701
[rfc6761]: https://www.rfc-editor.org/rfc/rfc6761 [ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?closed=1
[rfc6761]: https://www.rfc-editor.org/rfc/rfc6761
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
@@ -183,7 +224,7 @@ See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
- The ability to set custom IP for EDNS Client Subnet by using the new - The ability to set custom IP for EDNS Client Subnet by using the new
`dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip` `dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip`
fields ([#1472]). The UI changes are coming in the upcoming releases. properties ([#1472]). The UI changes are coming in the upcoming releases.
- The ability to use `dnstype` rules in the disallowed domains list ([#5468]). - The ability to use `dnstype` rules in the disallowed domains list ([#5468]).
This allows dropping requests based on their question types. This allows dropping requests based on their question types.
@@ -211,7 +252,7 @@ See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
``` ```
To rollback this change, move the value of `dns.edns_client_subnet.enabled` To rollback this change, move the value of `dns.edns_client_subnet.enabled`
into the `dns.edns_client_subnet`, remove the fields into the `dns.edns_client_subnet`, remove the properties
`dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`, `dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`,
`dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to `dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to
`16`. `16`.
@@ -271,11 +312,11 @@ See also the [v0.107.24 GitHub milestone][ms-v0.107.24].
### Added ### Added
- The ability to disable statistics by using the new `statistics.enabled` - The ability to disable statistics by using the new `statistics.enabled`
field. Previously it was necessary to set the `statistics_interval` to 0, property. Previously it was necessary to set the `statistics_interval` to 0,
losing the previous value ([#1717], [#4299]). losing the previous value ([#1717], [#4299]).
- The ability to exclude domain names from the query log or statistics by using - The ability to exclude domain names from the query log or statistics by using
the new `querylog.ignored` or `statistics.ignored` fields ([#1717], [#4299]). the new `querylog.ignored` or `statistics.ignored` properties ([#1717],
The UI changes are coming in the upcoming releases. [#4299]). The UI changes are coming in the upcoming releases.
### Changed ### Changed
@@ -300,7 +341,7 @@ In this release, the schema version has changed from 14 to 16.
To rollback this change, move the property back into the `dns` object and To rollback this change, move the property back into the `dns` object and
change the `schema_version` back to `15`. change the `schema_version` back to `15`.
- The fields `dns.querylog_enabled`, `dns.querylog_file_enabled`, - The properties `dns.querylog_enabled`, `dns.querylog_file_enabled`,
`dns.querylog_interval`, and `dns.querylog_size_memory` have been moved to the `dns.querylog_interval`, and `dns.querylog_size_memory` have been moved to the
new `querylog` object. new `querylog` object.
@@ -358,8 +399,8 @@ See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
### Added ### Added
- DNS64 support ([#5117]). The function may be enabled with new `use_dns64` - DNS64 support ([#5117]). The function may be enabled with new `use_dns64`
field under `dns` object in the configuration along with `dns64_prefixes`, the property under `dns` object in the configuration along with `dns64_prefixes`,
set of exclusion prefixes to filter AAAA responses. The Well-Known Prefix the set of exclusion prefixes to filter AAAA responses. The Well-Known Prefix
(`64:ff9b::/96`) is used if no custom prefixes are specified. (`64:ff9b::/96`) is used if no custom prefixes are specified.
### Fixed ### Fixed
@@ -1017,7 +1058,7 @@ In this release, the schema version has changed from 12 to 14.
hosts: true hosts: true
``` ```
The value for `clients.runtime_sources.rdns` field is taken from The value for `clients.runtime_sources.rdns` property is taken from
`dns.resolve_clients` property. To rollback this change, remove the `dns.resolve_clients` property. To rollback this change, remove the
`runtime_sources` property, move the contents of `persistent` into the `runtime_sources` property, move the contents of `persistent` into the
`clients` itself, the value of `clients.runtime_sources.rdns` into the `clients` itself, the value of `clients.runtime_sources.rdns` into the
@@ -1267,7 +1308,7 @@ See also the [v0.107.0 GitHub milestone][ms-v0.107.0].
log entries concerning cached responses won't include that information. log entries concerning cached responses won't include that information.
- Finnish and Ukrainian localizations. - Finnish and Ukrainian localizations.
- Setting the timeout for IP address pinging in the "Fastest IP address" mode - Setting the timeout for IP address pinging in the "Fastest IP address" mode
through the new `fastest_timeout` field in the configuration file ([#1992]). through the new `fastest_timeout` property in the configuration file ([#1992]).
- Static IP address detection on FreeBSD ([#3289]). - Static IP address detection on FreeBSD ([#3289]).
- Optimistic cache ([#2145]). - Optimistic cache ([#2145]).
- New possible value of `6h` for `querylog_interval` property ([#2504]). - New possible value of `6h` for `querylog_interval` property ([#2504]).
@@ -1683,7 +1724,7 @@ See also the [v0.105.2 GitHub milestone][ms-v0.105.2].
- Inconsistent responses for messages with EDNS0 and AD when DNS caching is - Inconsistent responses for messages with EDNS0 and AD when DNS caching is
enabled ([#2600]). enabled ([#2600]).
- Incomplete OpenWrt detection ([#2757]). - Incomplete OpenWrt detection ([#2757]).
- DHCP lease's `expired` field incorrect time format ([#2692]). - DHCP lease's `expired` property incorrect time format ([#2692]).
- Incomplete DNS upstreams validation ([#2674]). - Incomplete DNS upstreams validation ([#2674]).
- Wrong parsing of DHCP options of the `ip` type ([#2688]). - Wrong parsing of DHCP options of the `ip` type ([#2688]).
@@ -1720,8 +1761,8 @@ See also the [v0.105.1 GitHub milestone][ms-v0.105.1].
the machine has a static IP. the machine has a static IP.
- Optical issue on custom rules ([#2641]). - Optical issue on custom rules ([#2641]).
- Occasional crashes during startup. - Occasional crashes during startup.
- The field `"range_start"` in the `GET /control/dhcp/status` HTTP API response - The property `"range_start"` in the `GET /control/dhcp/status` HTTP API
is now correctly named again ([#2678]). response is now correctly named again ([#2678]).
- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` properties aren't reset - DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` properties aren't reset
to `false` on update anymore ([#2653]). to `false` on update anymore ([#2653]).
- The `Vary` header is now added along with `Access-Control-Allow-Origin` to - The `Vary` header is now added along with `Access-Control-Allow-Origin` to
@@ -1791,7 +1832,7 @@ See also the [v0.105.0 GitHub milestone][ms-v0.105.0].
- Go 1.14 support. v0.106.0 will require at least Go 1.15 to build. - Go 1.14 support. v0.106.0 will require at least Go 1.15 to build.
- The `darwin/386` port. It will be removed in v0.106.0. - The `darwin/386` port. It will be removed in v0.106.0.
- The `"rule"` and `"filter_id"` fields in `GET /filtering/check_host` and - The `"rule"` and `"filter_id"` property in `GET /filtering/check_host` and
`GET /querylog` responses. They will be removed in v0.106.0 ([#2102]). `GET /querylog` responses. They will be removed in v0.106.0 ([#2102]).
### Fixed ### Fixed
@@ -1899,11 +1940,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...HEAD
[v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28 [v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29
--> -->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD
[v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28
[v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27 [v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26 [v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26
[v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25 [v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25

2
client/dev.eslintrc vendored
View File

@@ -3,4 +3,4 @@
"rules": { "rules": {
"no-debugger":"warn", "no-debugger":"warn",
} }
} }

View File

@@ -12,11 +12,40 @@
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279"> <link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48"> <link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
<title>AdGuard Home</title> <title>AdGuard Home</title>
<style>
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
[data-theme="DARK"] .wrapper {
background-color: #f5f7fb;
}
</style>
</head> </head>
<body> <body>
<noscript> <noscript>
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<div id="root"></div> <div id="root">
<div class="wrapper"></div>
</div>
<script>
(function() {
var LOCAL_STORAGE_THEME_KEY = 'account_theme';
var theme = 'light';
try {
theme = window.localStorage.getItem(LOCAL_STORAGE_THEME_KEY);
} catch(e) {
console.error(e);
}
document.body.dataset.theme = theme;
})();
</script>
</body> </body>
</html> </html>

View File

@@ -17,5 +17,12 @@
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<div id="root"></div> <div id="root"></div>
<script>
(function() {
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var currentTheme = prefersDark ? 'dark' : 'light';
document.body.dataset.theme = currentTheme;
})();
</script>
</body> </body>
</html> </html>

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "The query log has been successfully cleared", "query_log_cleared": "The query log has been successfully cleared",
"query_log_updated": "The query log has been successfully updated", "query_log_updated": "The query log has been successfully updated",
"query_log_clear": "Clear query logs", "query_log_clear": "Clear query logs",
"query_log_retention": "Query logs retention", "query_log_retention": "Query logs rotation",
"query_log_enable": "Enable log", "query_log_enable": "Enable log",
"query_log_configuration": "Logs configuration", "query_log_configuration": "Logs configuration",
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
"query_log_strict_search": "Use double quotes for strict search", "query_log_strict_search": "Use double quotes for strict search",
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", "query_log_retention_confirm": "Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost",
"anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip": "Anonymize client IP",
"anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics", "anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics",
"dns_config": "DNS server configuration", "dns_config": "DNS server configuration",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Disable protection for {{count}} hour", "disable_notify_for_hours": "Disable protection for {{count}} hour",
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours", "disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
"disable_notify_until_tomorrow": "Disable protection until tomorrow", "disable_notify_until_tomorrow": "Disable protection until tomorrow",
"enable_protection_timer": "Protection will be enabled in {{time}}" "enable_protection_timer": "Protection will be enabled in {{time}}",
"custom_retention_input": "Enter retention in hours",
"custom_rotation_input": "Enter rotation in hours",
"protection_section_label": "Protection",
"log_and_stats_section_label": "Query log and statistics",
"ignore_query_log": "Ignore this client in query log",
"ignore_statistics": "Ignore this client in statistics"
} }

View File

@@ -167,6 +167,7 @@
"enabled_parental_toast": "Omogućen roditeljski nadzor", "enabled_parental_toast": "Omogućen roditeljski nadzor",
"disabled_safe_search_toast": "Onemogućeno sigurno pretraživanje", "disabled_safe_search_toast": "Onemogućeno sigurno pretraživanje",
"enabled_save_search_toast": "Omogućeno sigurno pretraživanje", "enabled_save_search_toast": "Omogućeno sigurno pretraživanje",
"updated_save_search_toast": "Ažurirane postavke sigurnog pretraživanja",
"enabled_table_header": "Omogućeno", "enabled_table_header": "Omogućeno",
"name_table_header": "Naziv", "name_table_header": "Naziv",
"list_url_table_header": "URL popisa", "list_url_table_header": "URL popisa",
@@ -290,6 +291,8 @@
"rate_limit": "Ograničenje", "rate_limit": "Ograničenje",
"edns_enable": "Omogući podmrežu klijenta EDNS-a", "edns_enable": "Omogući podmrežu klijenta EDNS-a",
"edns_cs_desc": "Dodajte opciju EDNS klijentske podmreže (ECS) uzvodnim zahtjevima i zabilježite vrijednosti koje su klijenti poslali u dnevnik upita.", "edns_cs_desc": "Dodajte opciju EDNS klijentske podmreže (ECS) uzvodnim zahtjevima i zabilježite vrijednosti koje su klijenti poslali u dnevnik upita.",
"edns_use_custom_ip": "Koristi prilagođeni IP za EDNS",
"edns_use_custom_ip_desc": "Dopusti korištenje prilagođenog IP-a za EDNS",
"rate_limit_desc": "Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.", "rate_limit_desc": "Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.",
"blocking_ipv4_desc": "Povratna IP adresa za blokirane A zahtjeve", "blocking_ipv4_desc": "Povratna IP adresa za blokirane A zahtjeve",
"blocking_ipv6_desc": "Povratna IP adresa za blokirane AAAA zahtjeve", "blocking_ipv6_desc": "Povratna IP adresa za blokirane AAAA zahtjeve",
@@ -523,6 +526,10 @@
"statistics_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni", "statistics_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
"statistics_cleared": "Statistika je uspješno uklonjenja", "statistics_cleared": "Statistika je uspješno uklonjenja",
"statistics_enable": "Omogući statistiku", "statistics_enable": "Omogući statistiku",
"ignore_domains": "Zanemarene domene (odvojene novim retkom)",
"ignore_domains_title": "Zanemarene domene",
"ignore_domains_desc_stats": "Upiti za ove domene ne upisuju se u statistiku",
"ignore_domains_desc_query": "Upiti za te domene nisu zapisani u zapisnik upita",
"interval_hours": "{{count}} sata/i", "interval_hours": "{{count}} sata/i",
"interval_hours_plural": "{{count}} sata/i", "interval_hours_plural": "{{count}} sata/i",
"filters_configuration": "Postavke filtara", "filters_configuration": "Postavke filtara",
@@ -642,5 +649,24 @@
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.", "anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.",
"confirm_dns_cache_clear": "Jeste li sigurni da želite očistiti DNS predmemoriju?", "confirm_dns_cache_clear": "Jeste li sigurni da želite očistiti DNS predmemoriju?",
"cache_cleared": "DNS predmemorija je uspješno izbrisana", "cache_cleared": "DNS predmemorija je uspješno izbrisana",
"clear_cache": "Očisti predmemoriju" "clear_cache": "Očisti predmemoriju",
"make_static": "Učini statičnim",
"theme_auto_desc": "Automatski (na temelju sheme boja vašeg uređaja)",
"theme_dark_desc": "Tamna tema",
"theme_light_desc": "Svijetla tema",
"disable_for_seconds": "Za {{count}} sekundi",
"disable_for_seconds_plural": "Za {{count}} sekundi",
"disable_for_minutes": "Za {{count}} minuta",
"disable_for_minutes_plural": "Za {{count}} minuta",
"disable_for_hours": "Za {{count}} sati",
"disable_for_hours_plural": "Za {{count}} sati",
"disable_until_tomorrow": "Do sutra",
"disable_notify_for_seconds": "Isključi zaštitu na {{count}} sekundi",
"disable_notify_for_seconds_plural": "Onemogući zaštitu na {{count}} sekundi",
"disable_notify_for_minutes": "Isključi zaštitu na {{count}} minuta",
"disable_notify_for_minutes_plural": "Isključi zaštitu na {{count}} minuta",
"disable_notify_for_hours": "Isključi zaštitu na {{count}} sati",
"disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati",
"disable_notify_until_tomorrow": "Isključi zaštitu do sutra",
"enable_protection_timer": "Zaštita će biti omogućena u {{time}}"
} }

View File

@@ -49,6 +49,9 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
const dnsStatus = await apiClient.getGlobalStatus(); const dnsStatus = await apiClient.getGlobalStatus();
if (dnsStatus) { if (dnsStatus) {
if (dnsStatus.protection_disabled_duration === 0) {
dnsStatus.protection_disabled_duration = null;
}
dispatch(dnsStatusSuccess(dnsStatus)); dispatch(dnsStatusSuccess(dnsStatus));
} }

View File

@@ -2,21 +2,6 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
export const getBlockedServicesAvailableServicesRequest = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_REQUEST');
export const getBlockedServicesAvailableServicesFailure = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_FAILURE');
export const getBlockedServicesAvailableServicesSuccess = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_SUCCESS');
export const getBlockedServicesAvailableServices = () => async (dispatch) => {
dispatch(getBlockedServicesAvailableServicesRequest());
try {
const data = await apiClient.getBlockedServicesAvailableServices();
dispatch(getBlockedServicesAvailableServicesSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getBlockedServicesAvailableServicesFailure());
}
};
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST'); export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');

View File

@@ -479,19 +479,12 @@ class Api {
} }
// Blocked services // Blocked services
BLOCKED_SERVICES_SERVICES = { path: 'blocked_services/services', method: 'GET' };
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' }; BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
getBlockedServicesAvailableServices() {
const { path, method } = this.BLOCKED_SERVICES_SERVICES;
return this.makeRequest(path, method);
}
getAllBlockedServices() { getAllBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_ALL; const { path, method } = this.BLOCKED_SERVICES_ALL;
return this.makeRequest(path, method); return this.makeRequest(path, method);

View File

@@ -165,8 +165,7 @@ const App = () => {
} }
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
const prefersDark = colorSchemeMedia.matches; setUITheme(theme);
setUITheme(prefersDark ? THEMES.dark : THEMES.light);
if (colorSchemeMedia.addEventListener !== undefined) { if (colorSchemeMedia.addEventListener !== undefined) {
colorSchemeMedia.addEventListener('change', (e) => { colorSchemeMedia.addEventListener('change', (e) => {

View File

@@ -12,7 +12,6 @@ import { MODAL_TYPE } from '../../helpers/constants';
import { import {
getCurrentFilter, getCurrentFilter,
getObjDiff,
} from '../../helpers/helpers'; } from '../../helpers/helpers';
import filtersCatalog from '../../helpers/filters/filters'; import filtersCatalog from '../../helpers/filters/filters';
@@ -22,7 +21,7 @@ class DnsBlocklist extends Component {
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} }
handleSubmit = (values, _, { initialValues }) => { handleSubmit = (values) => {
const { modalFilterUrl, modalType } = this.props.filtering; const { modalFilterUrl, modalType } = this.props.filtering;
switch (modalType) { switch (modalType) {
@@ -35,7 +34,12 @@ class DnsBlocklist extends Component {
break; break;
} }
case MODAL_TYPE.CHOOSE_FILTERING_LIST: { case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
const changedValues = getObjDiff(initialValues, values); const changedValues = Object.entries(values)?.reduce((acc, [key, value]) => {
if (value && key in filtersCatalog.filters) {
acc[key] = value;
}
return acc;
}, {});
Object.keys(changedValues) Object.keys(changedValues)
.forEach((fieldName) => { .forEach((fieldName) => {

View File

@@ -41,6 +41,17 @@ const settingsCheckboxes = [
placeholder: 'use_adguard_parental', placeholder: 'use_adguard_parental',
}, },
]; ];
const logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
const validate = (values) => { const validate = (values) => {
const errors = {}; const errors = {};
const { name, ids } = values; const { name, ids } = values;
@@ -148,6 +159,9 @@ let Form = (props) => {
settings: { settings: {
title: 'settings', title: 'settings',
component: <div label="settings" title={props.t('main_settings')}> component: <div label="settings" title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">
{t('protection_section_label')}
</div>
{settingsCheckboxes.map((setting) => ( {settingsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}> <div className="form__group" key={setting.name}>
<Field <Field
@@ -185,6 +199,19 @@ let Form = (props) => {
</div> </div>
))} ))}
</div> </div>
<div className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</div>
))}
</div>, </div>,
}, },
block_services: { block_services: {

View File

@@ -1,25 +1,37 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import {
change,
Field,
formValueSelector,
reduxForm,
} from 'redux-form';
import { connect } from 'react-redux';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { import {
CheckboxField, CheckboxField,
renderRadioField,
toFloatNumber, toFloatNumber,
renderTextareaField, renderTextareaField, renderInputField, renderRadioField,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { import {
FORM_NAME, FORM_NAME,
QUERY_LOG_INTERVALS_DAYS, QUERY_LOG_INTERVALS_DAYS,
HOUR, HOUR,
DAY, DAY,
RETENTION_CUSTOM,
RETENTION_CUSTOM_INPUT,
RETENTION_RANGE,
CUSTOM_INTERVAL,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import '../FormButton.css'; import '../FormButton.css';
const getIntervalTitle = (interval, t) => { const getIntervalTitle = (interval, t) => {
switch (interval) { switch (interval) {
case RETENTION_CUSTOM:
return t('settings_custom');
case 6 * HOUR: case 6 * HOUR:
return t('interval_6_hour'); return t('interval_6_hour');
case DAY: case DAY:
@@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.
/> />
)); ));
const Form = (props) => { let Form = (props) => {
const { const {
handleSubmit, submitting, invalid, processing, processingClear, handleClear, t, handleSubmit,
submitting,
invalid,
processing,
processingClear,
handleClear,
t,
interval,
customInterval,
dispatch,
} = props; } = props;
useEffect(() => {
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
}
}, [interval]);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
@@ -73,6 +100,37 @@ const Form = (props) => {
</label> </label>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field
key={RETENTION_CUSTOM}
name="interval"
type="radio"
component={renderRadioField}
value={QUERY_LOG_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_rotation_input')}
</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
type="number"
className="form-control"
component={renderInputField}
disabled={processing}
normalize={toFloatNumber}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
/>
</div>
)}
{getIntervalFields(processing, t, toFloatNumber)} {getIntervalFields(processing, t, toFloatNumber)}
</div> </div>
</div> </div>
@@ -96,7 +154,12 @@ const Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing} disabled={
submitting
|| invalid
|| processing
|| (!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
}
> >
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
@@ -121,8 +184,22 @@ Form.propTypes = {
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,
processingClear: PropTypes.bool.isRequired, processingClear: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
}; };
const selector = formValueSelector(FORM_NAME.LOG_CONFIG);
Form = connect((state) => {
const interval = selector(state, 'interval');
const customInterval = selector(state, CUSTOM_INTERVAL);
return {
interval,
customInterval,
};
})(Form);
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ form: FORM_NAME.LOG_CONFIG }), reduxForm({ form: FORM_NAME.LOG_CONFIG }),

View File

@@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Form from './Form'; import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class LogsConfig extends Component { class LogsConfig extends Component {
handleFormSubmit = (values) => { handleFormSubmit = (values) => {
const { t, interval: prevInterval } = this.props; const { t, interval: prevInterval } = this.props;
const { interval } = values; const { interval, customInterval, ...rest } = values;
const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] }; const newInterval = customInterval ? customInterval * HOUR : interval;
if (interval !== prevInterval) { const data = {
...rest,
ignored: values.ignored ? values.ignored.split('\n') : [],
interval: newInterval,
};
if (newInterval < prevInterval) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(t('query_log_retention_confirm'))) { if (window.confirm(t('query_log_retention_confirm'))) {
this.props.setLogsConfig(data); this.props.setLogsConfig(data);
@@ -32,7 +39,14 @@ class LogsConfig extends Component {
render() { render() {
const { const {
t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored, t,
enabled,
interval,
processing,
processingClear,
anonymize_client_ip,
ignored,
customInterval,
} = this.props; } = this.props;
return ( return (
@@ -46,6 +60,7 @@ class LogsConfig extends Component {
initialValues={{ initialValues={{
enabled, enabled,
interval, interval,
customInterval,
anonymize_client_ip, anonymize_client_ip,
ignored: ignored.join('\n'), ignored: ignored.join('\n'),
}} }}
@@ -62,6 +77,7 @@ class LogsConfig extends Component {
LogsConfig.propTypes = { LogsConfig.propTypes = {
interval: PropTypes.number.isRequired, interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
anonymize_client_ip: PropTypes.bool.isRequired, anonymize_client_ip: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,

View File

@@ -18,6 +18,11 @@
font-size: 14px; font-size: 14px;
} }
.form__group--input {
max-width: 300px;
margin: 0 1.5rem 10px;
}
.form__group--checkbox { .form__group--checkbox {
margin-bottom: 25px; margin-bottom: 25px;
} }
@@ -100,6 +105,14 @@
margin-bottom: 0; margin-bottom: 0;
} }
.form__label--bot {
margin-bottom: 10px;
}
.form__label--top {
margin-top: 10px;
}
.form__status { .form__status {
margin-top: 10px; margin-top: 10px;
font-size: 14px; font-size: 14px;

View File

@@ -1,32 +1,44 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import {
change, Field, formValueSelector, reduxForm,
} from 'redux-form';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { connect } from 'react-redux';
import { import {
renderRadioField, renderRadioField,
toNumber, toNumber,
CheckboxField, CheckboxField,
renderTextareaField, renderTextareaField,
toFloatNumber,
renderInputField,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { import {
FORM_NAME, FORM_NAME,
STATS_INTERVALS_DAYS, STATS_INTERVALS_DAYS,
DAY, DAY,
RETENTION_CUSTOM,
RETENTION_CUSTOM_INPUT,
CUSTOM_INTERVAL,
RETENTION_RANGE,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import '../FormButton.css'; import '../FormButton.css';
const getIntervalTitle = (intervalMs, t) => { const getIntervalTitle = (intervalMs, t) => {
switch (intervalMs / DAY) { switch (intervalMs) {
case 1: case RETENTION_CUSTOM:
return t('settings_custom');
case DAY:
return t('interval_24_hour'); return t('interval_24_hour');
default: default:
return t('interval_days', { count: intervalMs / DAY }); return t('interval_days', { count: intervalMs / DAY });
} }
}; };
const Form = (props) => { let Form = (props) => {
const { const {
handleSubmit, handleSubmit,
processing, processing,
@@ -35,8 +47,17 @@ const Form = (props) => {
handleReset, handleReset,
processingReset, processingReset,
t, t,
interval,
customInterval,
dispatch,
} = props; } = props;
useEffect(() => {
if (STATS_INTERVALS_DAYS.includes(interval)) {
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
}
}, [interval]);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
@@ -56,6 +77,37 @@ const Form = (props) => {
</div> </div>
<div className="form__group form__group--settings mt-2"> <div className="form__group form__group--settings mt-2">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field
key={RETENTION_CUSTOM}
name="interval"
type="radio"
component={renderRadioField}
value={STATS_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!STATS_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_retention_input')}
</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
type="number"
className="form-control"
component={renderInputField}
disabled={processing}
normalize={toFloatNumber}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
/>
</div>
)}
{STATS_INTERVALS_DAYS.map((interval) => ( {STATS_INTERVALS_DAYS.map((interval) => (
<Field <Field
key={interval} key={interval}
@@ -90,7 +142,12 @@ const Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing} disabled={
submitting
|| invalid
|| processing
|| (!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
}
> >
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
@@ -116,8 +173,22 @@ Form.propTypes = {
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,
processingReset: PropTypes.bool.isRequired, processingReset: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
}; };
const selector = formValueSelector(FORM_NAME.STATS_CONFIG);
Form = connect((state) => {
const interval = selector(state, 'interval');
const customInterval = selector(state, CUSTOM_INTERVAL);
return {
interval,
customInterval,
};
})(Form);
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ form: FORM_NAME.STATS_CONFIG }), reduxForm({ form: FORM_NAME.STATS_CONFIG }),

View File

@@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Form from './Form'; import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class StatsConfig extends Component { class StatsConfig extends Component {
handleFormSubmit = ({ enabled, interval, ignored }) => { handleFormSubmit = ({
enabled, interval, ignored, customInterval,
}) => {
const { t, interval: prevInterval } = this.props; const { t, interval: prevInterval } = this.props;
const newInterval = customInterval ? customInterval * HOUR : interval;
const config = { const config = {
enabled, enabled,
interval, interval: newInterval,
ignored: ignored ? ignored.split('\n') : [], ignored: ignored ? ignored.split('\n') : [],
}; };
@@ -33,7 +38,13 @@ class StatsConfig extends Component {
render() { render() {
const { const {
t, interval, processing, processingReset, ignored, enabled, t,
interval,
customInterval,
processing,
processingReset,
ignored,
enabled,
} = this.props; } = this.props;
return ( return (
@@ -46,6 +57,7 @@ class StatsConfig extends Component {
<Form <Form
initialValues={{ initialValues={{
interval, interval,
customInterval,
enabled, enabled,
ignored: ignored.join('\n'), ignored: ignored.join('\n'),
}} }}
@@ -62,6 +74,7 @@ class StatsConfig extends Component {
StatsConfig.propTypes = { StatsConfig.propTypes = {
interval: PropTypes.number.isRequired, interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
ignored: PropTypes.array.isRequired, ignored: PropTypes.array.isRequired,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,

View File

@@ -124,6 +124,7 @@ class Settings extends Component {
enabled={queryLogs.enabled} enabled={queryLogs.enabled}
ignored={queryLogs.ignored} ignored={queryLogs.ignored}
interval={queryLogs.interval} interval={queryLogs.interval}
customInterval={queryLogs.customInterval}
anonymize_client_ip={queryLogs.anonymize_client_ip} anonymize_client_ip={queryLogs.anonymize_client_ip}
processing={queryLogs.processingSetConfig} processing={queryLogs.processingSetConfig}
processingClear={queryLogs.processingClear} processingClear={queryLogs.processingClear}
@@ -134,6 +135,7 @@ class Settings extends Component {
<div className="col-md-12"> <div className="col-md-12">
<StatsConfig <StatsConfig
interval={stats.interval} interval={stats.interval}
customInterval={stats.customInterval}
ignored={stats.ignored} ignored={stats.ignored}
enabled={stats.enabled} enabled={stats.enabled}
processing={stats.processingSetConfig} processing={stats.processingSetConfig}
@@ -166,6 +168,7 @@ Settings.propTypes = {
stats: PropTypes.shape({ stats: PropTypes.shape({
processingGetConfig: PropTypes.bool, processingGetConfig: PropTypes.bool,
interval: PropTypes.number, interval: PropTypes.number,
customInterval: PropTypes.number,
enabled: PropTypes.bool, enabled: PropTypes.bool,
ignored: PropTypes.array, ignored: PropTypes.array,
processingSetConfig: PropTypes.bool, processingSetConfig: PropTypes.bool,
@@ -174,6 +177,7 @@ Settings.propTypes = {
queryLogs: PropTypes.shape({ queryLogs: PropTypes.shape({
enabled: PropTypes.bool, enabled: PropTypes.bool,
interval: PropTypes.number, interval: PropTypes.number,
customInterval: PropTypes.number,
anonymize_client_ip: PropTypes.bool, anonymize_client_ip: PropTypes.bool,
processingSetConfig: PropTypes.bool, processingSetConfig: PropTypes.bool,
processingClear: PropTypes.bool, processingClear: PropTypes.bool,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import cn from 'classnames'; import cn from 'classnames';
@@ -33,16 +33,14 @@ const Footer = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const currentTheme = useSelector((state) => (state.dashboard ? state.dashboard.theme : 'auto')); const currentTheme = useSelector((state) => (
const profileName = useSelector((state) => (state.dashboard ? state.dashboard.name : '')); state.dashboard ? state.dashboard.theme : THEMES.auto
));
const profileName = useSelector((state) => (
state.dashboard ? state.dashboard.name : ''
));
const isLoggedIn = profileName !== ''; const isLoggedIn = profileName !== '';
const [currentThemeLocal, setCurrentThemeLocal] = useState('auto'); const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);
useEffect(() => {
if (!isLoggedIn) {
setUITheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.dark : THEMES.light);
}
}, []);
const getYear = () => { const getYear = () => {
const today = new Date(); const today = new Date();

View File

@@ -13,7 +13,7 @@
font-size: 28px; font-size: 28px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
background-color: rgba(255, 255, 255, 0.8); background-color: var(--rt-nodata-bgcolor);
} }
.overlay--visible { .overlay--visible {

View File

@@ -220,6 +220,12 @@ export const STATS_INTERVALS_DAYS = [DAY, DAY * 7, DAY * 30, DAY * 90];
export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90]; export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90];
export const RETENTION_CUSTOM = 1;
export const RETENTION_CUSTOM_INPUT = 'custom_retention_input';
export const CUSTOM_INTERVAL = 'customInterval';
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
// Note that translation strings contain these modes (blocking_mode_CONSTANT) // Note that translation strings contain these modes (blocking_mode_CONSTANT)
@@ -462,6 +468,11 @@ export const UINT32_RANGE = {
MAX: 4294967295, MAX: 4294967295,
}; };
export const RETENTION_RANGE = {
MIN: 1,
MAX: 365 * 24,
};
export const DHCP_VALUES_PLACEHOLDERS = { export const DHCP_VALUES_PLACEHOLDERS = {
ipv4: { ipv4: {
subnet_mask: '255.255.255.0', subnet_mask: '255.255.255.0',
@@ -537,3 +548,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
HOUR: 60 * 60 * 1000, HOUR: 60 * 60 * 1000,
TOMORROW: 24 * 60 * 60 * 1000, TOMORROW: 24 * 60 * 60 * 1000,
}; };
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';

View File

@@ -25,6 +25,8 @@ import {
STANDARD_HTTPS_PORT, STANDARD_HTTPS_PORT,
STANDARD_WEB_PORT, STANDARD_WEB_PORT,
SPECIAL_FILTER_ID, SPECIAL_FILTER_ID,
THEMES,
LOCAL_STORAGE_THEME_KEY,
} from './constants'; } from './constants';
/** /**
@@ -678,13 +680,61 @@ export const setHtmlLangAttr = (language) => {
window.document.documentElement.lang = language; window.document.documentElement.lang = language;
}; };
/**
* Set local storage field
*
* @param {string} key
* @param {string} value
*/
export const setStorageItem = (key, value) => {
if (window.localStorage) {
window.localStorage.setItem(key, value);
}
};
/**
* Get local storage field
*
* @param {string} key
*/
export const getStorageItem = (key) => (window.localStorage
? window.localStorage.getItem(key)
: null);
/**
* Set local storage theme field
*
* @param {string} theme
*/
export const setTheme = (theme) => {
setStorageItem(LOCAL_STORAGE_THEME_KEY, theme);
};
/**
* Get local storage theme field
*
* @returns {string}
*/
export const getTheme = () => getStorageItem(LOCAL_STORAGE_THEME_KEY) || THEMES.light;
/** /**
* Sets UI theme. * Sets UI theme.
* *
* @param theme * @param theme
*/ */
export const setUITheme = (theme) => { export const setUITheme = (theme) => {
document.body.dataset.theme = theme; let currentTheme = theme || getTheme();
if (currentTheme === THEMES.auto) {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
currentTheme = prefersDark ? THEMES.dark : THEMES.light;
}
setTheme(currentTheme);
document.body.dataset.theme = currentTheme;
}; };
/** /**

View File

@@ -1,5 +1,5 @@
{ {
"timeUpdated": "2023-03-29T00:10:35.167Z", "timeUpdated": "2023-04-06T10:46:09.881Z",
"categories": { "categories": {
"0": "audio_video_player", "0": "audio_video_player",
"1": "comments", "1": "comments",
@@ -24192,4 +24192,4 @@
"3gppnetwork.org": "3gpp", "3gppnetwork.org": "3gpp",
"3gpp.org": "3gpp" "3gpp.org": "3gpp"
} }
} }

View File

@@ -8,6 +8,7 @@ import * as actionCreators from '../../actions/login';
import logo from '../../components/ui/svg/logo.svg'; import logo from '../../components/ui/svg/logo.svg';
import Toasts from '../../components/Toasts'; import Toasts from '../../components/Toasts';
import Footer from '../../components/ui/Footer'; import Footer from '../../components/ui/Footer';
import Icons from '../../components/ui/Icons';
import Form from './Form'; import Form from './Form';
import './Login.css'; import './Login.css';
@@ -69,6 +70,7 @@ class Login extends Component {
</div> </div>
<Footer /> <Footer />
<Toasts /> <Toasts />
<Icons />
</div> </div>
); );
} }

View File

@@ -177,7 +177,7 @@ const dashboard = handleActions(
autoClients: [], autoClients: [],
supportedTags: [], supportedTags: [],
name: '', name: '',
theme: 'auto', theme: undefined,
checkUpdateFlag: false, checkUpdateFlag: false,
}, },
); );

View File

@@ -1,7 +1,9 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import * as actions from '../actions/queryLogs'; import * as actions from '../actions/queryLogs';
import { DEFAULT_LOGS_FILTER, DAY } from '../helpers/constants'; import {
DEFAULT_LOGS_FILTER, DAY, QUERY_LOG_INTERVALS_DAYS, HOUR,
} from '../helpers/constants';
const queryLogs = handleActions( const queryLogs = handleActions(
{ {
@@ -59,6 +61,9 @@ const queryLogs = handleActions(
[actions.getLogsConfigSuccess]: (state, { payload }) => ({ [actions.getLogsConfigSuccess]: (state, { payload }) => ({
...state, ...state,
...payload, ...payload,
customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval)
? payload.interval / HOUR
: null,
processingGetConfig: false, processingGetConfig: false,
}), }),
@@ -95,6 +100,7 @@ const queryLogs = handleActions(
anonymize_client_ip: false, anonymize_client_ip: false,
isDetailed: true, isDetailed: true,
isEntireLog: false, isEntireLog: false,
customInterval: null,
}, },
); );

View File

@@ -1,6 +1,6 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import { normalizeTopClients } from '../helpers/helpers'; import { normalizeTopClients } from '../helpers/helpers';
import { DAY } from '../helpers/constants'; import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants';
import * as actions from '../actions/stats'; import * as actions from '../actions/stats';
@@ -27,6 +27,9 @@ const stats = handleActions(
[actions.getStatsConfigSuccess]: (state, { payload }) => ({ [actions.getStatsConfigSuccess]: (state, { payload }) => ({
...state, ...state,
...payload, ...payload,
customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval)
? payload.interval / HOUR
: null,
processingGetConfig: false, processingGetConfig: false,
}), }),
@@ -93,6 +96,7 @@ const stats = handleActions(
processingStats: true, processingStats: true,
processingReset: false, processingReset: false,
interval: DAY, interval: DAY,
customInterval: null,
...defaultStats, ...defaultStats,
}, },
); );

View File

@@ -4,19 +4,27 @@
/^[[:space:]]+- .+/ { /^[[:space:]]+- .+/ {
if (FNR - prev_line == 1) { if (FNR - prev_line == 1) {
addrs[addrsnum++] = $2 addrs[$2] = true
prev_line = FNR prev_line = FNR
if ($2 == "0.0.0.0" || $2 == "::") {
delete addrs
addrs["localhost"] = true
# Drop all the other addresses.
prev_line = -1
}
} }
} }
/^[[:space:]]+port:/ { if (is_dns) port = $2 } /^[[:space:]]+port:/ { if (is_dns) port = $2 }
END { END {
for (i in addrs) { for (addr in addrs) {
if (match(addrs[i], ":")) { if (match(addr, ":")) {
print "[" addrs[i] "]:" port print "[" addr "]:" port
} else { } else {
print addrs[i] ":" port print addr ":" port
} }
} }
} }

View File

@@ -10,4 +10,4 @@ END {
} else { } else {
print "http://" host ":" port print "http://" host ":" port
} }
} }

28
go.mod
View File

@@ -4,10 +4,10 @@ go 1.19
require ( require (
github.com/AdguardTeam/dnsproxy v0.48.3 github.com/AdguardTeam/dnsproxy v0.48.3
github.com/AdguardTeam/golibs v0.13.1 github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/urlfilter v0.16.1 github.com/AdguardTeam/urlfilter v0.16.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.6 github.com/ameshkov/dnscrypt/v2 v2.2.7
github.com/digineo/go-ipset/v2 v2.2.1 github.com/digineo/go-ipset/v2 v2.2.1
github.com/dimfeld/httptreemux/v5 v5.5.0 github.com/dimfeld/httptreemux/v5 v5.5.0
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
@@ -17,19 +17,23 @@ require (
github.com/google/renameio v1.0.1 github.com/google/renameio v1.0.1
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.2 github.com/kardianos/service v1.2.2
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
github.com/mdlayher/netlink v1.7.1 github.com/mdlayher/netlink v1.7.1
github.com/mdlayher/packet v1.1.1 github.com/mdlayher/packet v1.1.1
// TODO(a.garipov): This package is deprecated; find a new one or use our
// own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.53 github.com/miekg/dns v1.1.53
github.com/quic-go/quic-go v0.33.0 github.com/quic-go/quic-go v0.33.0
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
github.com/ti-mo/netfilter v0.5.0 github.com/ti-mo/netfilter v0.5.0
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.8.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/net v0.8.0 golang.org/x/net v0.9.0
golang.org/x/sys v0.6.0 golang.org/x/sys v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@@ -44,9 +48,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/raw v0.1.0 // indirect
github.com/mdlayher/socket v0.4.0 // indirect github.com/mdlayher/socket v0.4.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.2 // indirect github.com/onsi/ginkgo/v2 v2.9.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
@@ -54,11 +56,11 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.0 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
golang.org/x/mod v0.9.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.7.0 // indirect golang.org/x/tools v0.8.0 // indirect
) )

49
go.sum
View File

@@ -2,8 +2,8 @@ github.com/AdguardTeam/dnsproxy v0.48.3 h1:h9xgDSmd1MqsPFNApyaPVXolmSTtzOWOcfWvP
github.com/AdguardTeam/dnsproxy v0.48.3/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw= github.com/AdguardTeam/dnsproxy v0.48.3/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.13.1 h1:x6ChoXk2jborbCWJ01TyBAEY3SilHts0SCG7yjnf6Sc= github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
github.com/AdguardTeam/golibs v0.13.1/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8= github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw= github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI= github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
@@ -15,8 +15,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ameshkov/dnscrypt/v2 v2.2.6 h1:rE7AFbPWebq7me7RVS66Cipd1m7ef1yf2+C8QzjQXXE= github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw=
github.com/ameshkov/dnscrypt/v2 v2.2.6/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow= github.com/ameshkov/dnscrypt/v2 v2.2.7/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
@@ -55,8 +55,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -70,8 +70,8 @@ github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe0
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
@@ -123,10 +123,10 @@ 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E= github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA= github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
@@ -161,15 +161,15 @@ go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -185,8 +185,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@@ -218,23 +218,24 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@@ -52,7 +53,7 @@ const textPlainDeprMsg = `using this api with the text/plain content-type is dep
// deprecation and removal of a plain-text API if the request is made with the // deprecation and removal of a plain-text API if the request is made with the
// "text/plain" content-type. // "text/plain" content-type.
func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) { func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) {
if r.Header.Get(HdrNameContentType) != HdrValTextPlain { if r.Header.Get(httphdr.ContentType) != HdrValTextPlain {
return false return false
} }
@@ -72,7 +73,7 @@ func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err er
// redefine the status code. // redefine the status code.
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) { func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
w.WriteHeader(code) w.WriteHeader(code)
w.Header().Set(HdrNameContentType, HdrValApplicationJSON) w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
err = json.NewEncoder(w).Encode(resp) err = json.NewEncoder(w).Encode(resp)
if err != nil { if err != nil {
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err) Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)

View File

@@ -1,22 +1,6 @@
package aghhttp package aghhttp
// HTTP Headers // HTTP headers
// HTTP header name constants.
//
// TODO(a.garipov): Remove unused.
const (
HdrNameAcceptEncoding = "Accept-Encoding"
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HdrNameAltSvc = "Alt-Svc"
HdrNameContentEncoding = "Content-Encoding"
HdrNameContentType = "Content-Type"
HdrNameOrigin = "Origin"
HdrNameServer = "Server"
HdrNameTrailer = "Trailer"
HdrNameUserAgent = "User-Agent"
HdrNameVary = "Vary"
)
// HTTP header value constants. // HTTP header value constants.
const ( const (

View File

@@ -35,4 +35,4 @@
1.3.5.7 domain4 domain4.alias 1.3.5.7 domain4 domain4.alias
7.5.3.1 domain4.alias domain4 7.5.3.1 domain4.alias domain4
::13 domain6 domain6.alias ::13 domain6 domain6.alias
::31 domain6.alias domain6 ::31 domain6.alias domain6

View File

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

View File

@@ -2,4 +2,4 @@
# parent directory. Real interface files usually contain only absolute paths. # parent directory. Real interface files usually contain only absolute paths.
source ./testdata/ifaces source ./testdata/ifaces
source ./testdata/* source ./testdata/*

View File

@@ -3,4 +3,4 @@ IP address HW type Flags HW address Mask Device
::ffff:ffff 0x1 0x0 ef:cd:ab:ef:cd:ab * br-lan ::ffff:ffff 0x1 0x0 ef:cd:ab:ef:cd:ab * br-lan
0.0.0.0 0x0 0x0 00:00:00:00:00:00 * unspec 0.0.0.0 0x0 0x0 00:00:00:00:00:00 * unspec
1.2.3.4.5 0x1 0x2 aa:bb:cc:dd:ee:ff * wan 1.2.3.4.5 0x1 0x2 aa:bb:cc:dd:ee:ff * wan
1.2.3.4 0x1 0x2 12:34:56:78:910 * wan 1.2.3.4 0x1 0x2 12:34:56:78:910 * wan

View File

@@ -1,10 +0,0 @@
//go:build mips || mips64
// This file is an adapted version of github.com/josharian/native.
package aghos
import "encoding/binary"
// NativeEndian is the native endianness of this system.
var NativeEndian = binary.BigEndian

View File

@@ -1,10 +0,0 @@
//go:build amd64 || 386 || arm || arm64 || mipsle || mips64le || ppc64le
// This file is an adapted version of github.com/josharian/native.
package aghos
import "encoding/binary"
// NativeEndian is the native endianness of this system.
var NativeEndian = binary.LittleEndian

View File

@@ -0,0 +1,293 @@
//go:build darwin
package dhcpd
import (
"fmt"
"net"
"os"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/ethernet"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
// the unconfigured host.
type dhcpUnicastAddr struct {
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
// actually implementing all methods. It also contains the client's
// hardware address.
raw.Addr
// yiaddr is an IP address just allocated by server for the host.
yiaddr net.IP
}
// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and
// net.HardwareAddr.
type dhcpConn struct {
// udpConn is the connection for UDP addresses.
udpConn net.PacketConn
// bcastIP is the broadcast address specific for the configured
// interface's subnet.
bcastIP net.IP
// rawConn is the connection for MAC addresses.
rawConn net.PacketConn
// srcMAC is the hardware address of the configured network interface.
srcMAC net.HardwareAddr
// srcIP is the IP address of the configured network interface.
srcIP net.IP
}
// newDHCPConn creates the special connection for DHCP server.
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
var ucast net.PacketConn
if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
return nil, fmt.Errorf("creating raw udp connection: %w", err)
}
// Create the UDP connection.
var bcast net.PacketConn
bcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{
// TODO(e.burkov): Listening on zeroes makes the server handle
// requests from all the interfaces. Inspect the ways to
// specify the interface-specific listening addresses.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
IP: net.IP{0, 0, 0, 0},
Port: dhcpv4.ServerPort,
})
if err != nil {
return nil, fmt.Errorf("creating ipv4 udp connection: %w", err)
}
return &dhcpConn{
udpConn: bcast,
bcastIP: s.conf.broadcastIP.AsSlice(),
rawConn: ucast,
srcMAC: iface.HardwareAddr,
srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
}, nil
}
// wrapErrs is a helper to wrap the errors from two independent underlying
// connections.
func (*dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
switch {
case udpConnErr != nil && rawConnErr != nil:
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
case udpConnErr != nil:
return fmt.Errorf("%s udp connection: %w", action, udpConnErr)
case rawConnErr != nil:
return fmt.Errorf("%s raw connection: %w", action, rawConnErr)
default:
return nil
}
}
// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying
// connection to write to based on the type of addr.
func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
switch addr := addr.(type) {
case *dhcpUnicastAddr:
// Unicast the message to the client's MAC address. Use the raw
// connection.
//
// Note: unicasting is performed on the only network interface
// that is configured. For now it may be not what users expect
// so additionally broadcast the message via UDP connection.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
var rerr error
n, rerr = c.unicast(p, addr)
_, uerr := c.broadcast(p, &net.UDPAddr{
IP: netutil.IPv4bcast(),
Port: dhcpv4.ClientPort,
})
return n, c.wrapErrs("writing to", uerr, rerr)
case *net.UDPAddr:
if addr.IP.Equal(net.IPv4bcast) {
// Broadcast the message for the client which supports
// it. Use the UDP connection.
return c.broadcast(p, addr)
}
// Unicast the message to the client's IP address. Use the UDP
// connection.
return c.udpConn.WriteTo(p, addr)
default:
return 0, fmt.Errorf("addr has an unexpected type %T", addr)
}
}
// ReadFrom implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return c.udpConn.ReadFrom(p)
}
// unicast wraps respData with required frames and writes it to the peer.
func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {
var data []byte
data, err = c.buildEtherPkt(respData, peer)
if err != nil {
return 0, err
}
return c.rawConn.WriteTo(data, &peer.Addr)
}
// Close implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) Close() (err error) {
rerr := c.rawConn.Close()
if errors.Is(rerr, os.ErrClosed) {
// Ignore the error since the actual file is closed already.
rerr = nil
}
return c.wrapErrs("closing", c.udpConn.Close(), rerr)
}
// LocalAddr implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) LocalAddr() (a net.Addr) {
return c.udpConn.LocalAddr()
}
// SetDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetDeadline(t time.Time) (err error) {
return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))
}
// SetReadDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetReadDeadline(t time.Time) error {
return c.wrapErrs(
"setting reading deadline on",
c.udpConn.SetReadDeadline(t),
c.rawConn.SetReadDeadline(t),
)
}
// SetWriteDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
return c.wrapErrs(
"setting writing deadline on",
c.udpConn.SetWriteDeadline(t),
c.rawConn.SetWriteDeadline(t),
)
}
// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by
// RFC-1700.
//
// See https://datatracker.ietf.org/doc/html/rfc1700.
const ipv4DefaultTTL = 64
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.
// Validation of the payload is a caller's responsibility.
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
udpLayer := &layers.UDP{
SrcPort: dhcpv4.ServerPort,
DstPort: dhcpv4.ClientPort,
}
ipv4Layer := &layers.IPv4{
Version: uint8(layers.IPProtocolIPv4),
Flags: layers.IPv4DontFragment,
TTL: ipv4DefaultTTL,
Protocol: layers.IPProtocolUDP,
SrcIP: c.srcIP,
DstIP: peer.yiaddr,
}
// Ignore the error since it's only returned for invalid network layer's
// type.
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
ethLayer := &layers.Ethernet{
SrcMAC: c.srcMAC,
DstMAC: peer.HardwareAddr,
EthernetType: layers.EthernetTypeIPv4,
}
buf := gopacket.NewSerializeBuffer()
setts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
err = gopacket.SerializeLayers(
buf,
setts,
ethLayer,
ipv4Layer,
udpLayer,
gopacket.Payload(payload),
)
if err != nil {
return nil, fmt.Errorf("serializing layers: %w", err)
}
return buf.Bytes(), nil
}
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}

View File

@@ -0,0 +1,219 @@
//go:build darwin
package dhcpd
import (
"net"
"testing"
"github.com/AdguardTeam/golibs/testutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
func TestDHCPConn_WriteTo_common(t *testing.T) {
respData := (&dhcpv4.DHCPv4{}).ToBytes()
udpAddr := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4},
Port: dhcpv4.ClientPort,
}
t.Run("unicast_ip", func(t *testing.T) {
writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, udpAddr, addr)
return 0, nil
}
conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}
_, err := conn.WriteTo(respData, udpAddr)
assert.NoError(t, err)
})
t.Run("unexpected_addr_type", func(t *testing.T) {
type unexpectedAddrType struct {
net.Addr
}
conn := &dhcpConn{}
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
require.Error(t, err)
testutil.AssertErrorMsg(t, "addr has an unexpected type *dhcpd.unexpectedAddrType", err)
assert.Zero(t, n)
})
}
func TestBuildEtherPkt(t *testing.T) {
conn := &dhcpConn{
srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},
srcIP: net.IP{1, 2, 3, 4},
}
peer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
payload := (&dhcpv4.DHCPv4{}).ToBytes()
t.Run("success", func(t *testing.T) {
pkt, err := conn.buildEtherPkt(payload, peer)
require.NoError(t, err)
assert.NotEmpty(t, pkt)
actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{
NoCopy: true,
})
require.NotNil(t, actualPkt)
wantTypes := []gopacket.LayerType{
layers.LayerTypeEthernet,
layers.LayerTypeIPv4,
layers.LayerTypeUDP,
layers.LayerTypeDHCPv4,
}
actualLayers := actualPkt.Layers()
require.Len(t, actualLayers, len(wantTypes))
for i, wantType := range wantTypes {
layer := actualLayers[i]
require.NotNil(t, layer)
assert.Equal(t, wantType, layer.LayerType())
}
})
t.Run("bad_payload", func(t *testing.T) {
// Create an invalid DHCP packet.
invalidPayload := []byte{1, 2, 3, 4}
pkt, err := conn.buildEtherPkt(invalidPayload, peer)
require.NoError(t, err)
assert.NotEmpty(t, pkt)
})
t.Run("serializing_error", func(t *testing.T) {
// Create a peer with invalid MAC.
badPeer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
pkt, err := conn.buildEtherPkt(payload, badPeer)
require.Error(t, err)
assert.Empty(t, pkt)
})
}
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}

View File

@@ -1,4 +1,4 @@
//go:build darwin || freebsd || linux || openbsd //go:build freebsd || linux || openbsd
package dhcpd package dhcpd
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
@@ -238,3 +239,53 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}

View File

@@ -1,4 +1,4 @@
//go:build darwin || freebsd || linux || openbsd //go:build freebsd || linux || openbsd
package dhcpd package dhcpd
@@ -110,3 +110,108 @@ func TestBuildEtherPkt(t *testing.T) {
assert.Empty(t, pkt) assert.Empty(t, pkt)
}) })
} }
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}

View File

@@ -71,16 +71,17 @@ func (s *server) dbLoad() (err error) {
IP: ip, IP: ip,
Hostname: obj[i].Hostname, Hostname: obj[i].Hostname,
Expiry: time.Unix(obj[i].Expiry, 0), Expiry: time.Unix(obj[i].Expiry, 0),
IsStatic: obj[i].Expiry == leaseExpireStatic,
} }
if len(obj[i].IP) == 16 { if len(obj[i].IP) == 16 {
if obj[i].Expiry == leaseExpireStatic { if lease.IsStatic {
v6StaticLeases = append(v6StaticLeases, &lease) v6StaticLeases = append(v6StaticLeases, &lease)
} else { } else {
v6DynLeases = append(v6DynLeases, &lease) v6DynLeases = append(v6DynLeases, &lease)
} }
} else { } else {
if obj[i].Expiry == leaseExpireStatic { if lease.IsStatic {
staticLeases = append(staticLeases, &lease) staticLeases = append(staticLeases, &lease)
} else { } else {
dynLeases = append(dynLeases, &lease) dynLeases = append(dynLeases, &lease)

View File

@@ -51,6 +51,9 @@ type Lease struct {
// //
// TODO(a.garipov): Migrate leases.db. // TODO(a.garipov): Migrate leases.db.
IP netip.Addr `json:"ip"` IP netip.Addr `json:"ip"`
// IsStatic defines if the lease is static.
IsStatic bool `json:"static"`
} }
// Clone returns a deep copy of l. // Clone returns a deep copy of l.
@@ -64,6 +67,7 @@ func (l *Lease) Clone() (clone *Lease) {
Hostname: l.Hostname, Hostname: l.Hostname,
HWAddr: slices.Clone(l.HWAddr), HWAddr: slices.Clone(l.HWAddr),
IP: l.IP, IP: l.IP,
IsStatic: l.IsStatic,
} }
} }
@@ -84,17 +88,10 @@ func (l *Lease) IsBlocklisted() (ok bool) {
return true return true
} }
// IsStatic returns true if the lease is static.
//
// TODO(a.garipov): Just make it a boolean field.
func (l *Lease) IsStatic() (ok bool) {
return l != nil && l.Expiry.Unix() == leaseExpireStatic
}
// MarshalJSON implements the json.Marshaler interface for Lease. // MarshalJSON implements the json.Marshaler interface for Lease.
func (l Lease) MarshalJSON() ([]byte, error) { func (l Lease) MarshalJSON() ([]byte, error) {
var expiryStr string var expiryStr string
if !l.IsStatic() { if !l.IsStatic {
// The front-end is waiting for RFC 3999 format of the time // The front-end is waiting for RFC 3999 format of the time
// value. It also shouldn't got an Expiry field for static // value. It also shouldn't got an Expiry field for static
// leases. // leases.

View File

@@ -80,7 +80,7 @@ func TestDB(t *testing.T) {
assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr) assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
assert.Equal(t, leases[1].IP, ll[0].IP) assert.Equal(t, leases[1].IP, ll[0].IP)
assert.True(t, ll[0].IsStatic()) assert.True(t, ll[0].IsStatic)
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr) assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
assert.Equal(t, leases[0].IP, ll[1].IP) assert.Equal(t, leases[0].IP, ll[1].IP)

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"os" "os"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -57,12 +58,77 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
// dhcpStatusResponse is the response for /control/dhcp/status endpoint. // dhcpStatusResponse is the response for /control/dhcp/status endpoint.
type dhcpStatusResponse struct { type dhcpStatusResponse struct {
IfaceName string `json:"interface_name"` IfaceName string `json:"interface_name"`
V4 V4ServerConf `json:"v4"` V4 V4ServerConf `json:"v4"`
V6 V6ServerConf `json:"v6"` V6 V6ServerConf `json:"v6"`
Leases []*Lease `json:"leases"` Leases []*leaseDynamic `json:"leases"`
StaticLeases []*Lease `json:"static_leases"` StaticLeases []*leaseStatic `json:"static_leases"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
}
// leaseStatic is the JSON form of static DHCP lease.
type leaseStatic struct {
HWAddr string `json:"mac"`
IP netip.Addr `json:"ip"`
Hostname string `json:"hostname"`
}
// leasesToStatic converts list of leases to their JSON form.
func leasesToStatic(leases []*Lease) (static []*leaseStatic) {
static = make([]*leaseStatic, len(leases))
for i, l := range leases {
static[i] = &leaseStatic{
HWAddr: l.HWAddr.String(),
IP: l.IP,
Hostname: l.Hostname,
}
}
return static
}
// toLease converts leaseStatic to Lease or returns error.
func (l *leaseStatic) toLease() (lease *Lease, err error) {
addr, err := net.ParseMAC(l.HWAddr)
if err != nil {
return nil, fmt.Errorf("couldn't parse MAC address: %w", err)
}
return &Lease{
HWAddr: addr,
IP: l.IP,
Hostname: l.Hostname,
IsStatic: true,
}, nil
}
// leaseDynamic is the JSON form of dynamic DHCP lease.
type leaseDynamic struct {
HWAddr string `json:"mac"`
IP netip.Addr `json:"ip"`
Hostname string `json:"hostname"`
Expiry string `json:"expires"`
}
// leasesToDynamic converts list of leases to their JSON form.
func leasesToDynamic(leases []*Lease) (dynamic []*leaseDynamic) {
dynamic = make([]*leaseDynamic, len(leases))
for i, l := range leases {
dynamic[i] = &leaseDynamic{
HWAddr: l.HWAddr.String(),
IP: l.IP,
Hostname: l.Hostname,
// The front-end is waiting for RFC 3999 format of the time
// value.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
Expiry: l.Expiry.Format(time.RFC3339),
}
}
return dynamic
} }
func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
@@ -76,8 +142,8 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
s.srv4.WriteDiskConfig4(&status.V4) s.srv4.WriteDiskConfig4(&status.V4)
s.srv6.WriteDiskConfig6(&status.V6) s.srv6.WriteDiskConfig6(&status.V6)
status.Leases = s.Leases(LeasesDynamic) status.Leases = leasesToDynamic(s.Leases(LeasesDynamic))
status.StaticLeases = s.Leases(LeasesStatic) status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic))
_ = aghhttp.WriteJSONResponse(w, r, status) _ = aghhttp.WriteJSONResponse(w, r, status)
} }
@@ -488,7 +554,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
} }
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
l := &Lease{} l := &leaseStatic{}
err := json.NewDecoder(r.Body).Decode(l) err := json.NewDecoder(r.Body).Decode(l)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
@@ -511,7 +577,14 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
srv = s.srv6 srv = s.srv6
} }
err = srv.AddStaticLease(l) lease, err := l.toLease()
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "parsing: %s", err)
return
}
err = srv.AddStaticLease(lease)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -520,7 +593,7 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
} }
func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
l := &Lease{} l := &leaseStatic{}
err := json.NewDecoder(r.Body).Decode(l) err := json.NewDecoder(r.Body).Decode(l)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
@@ -543,7 +616,14 @@ func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
srv = s.srv6 srv = s.srv6
} }
err = srv.RemoveStaticLease(l) lease, err := l.toLease()
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "parsing: %s", err)
return
}
err = srv.RemoveStaticLease(lease)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

View File

@@ -5,28 +5,27 @@ package dhcpd
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestServer_handleDHCPStatus(t *testing.T) { func TestServer_handleDHCPStatus(t *testing.T) {
const staticName = "static-client" const (
staticName = "static-client"
staticMAC = "aa:aa:aa:aa:aa:aa"
)
staticIP := netip.MustParseAddr("192.168.10.10") staticIP := netip.MustParseAddr("192.168.10.10")
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
staticLease := &Lease{ staticLease := &leaseStatic{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
Hostname: staticName,
} }
s, err := Create(&ServerConfig{ s, err := Create(&ServerConfig{
@@ -65,8 +64,8 @@ func TestServer_handleDHCPStatus(t *testing.T) {
resp := &dhcpStatusResponse{ resp := &dhcpStatusResponse{
V4: *conf4, V4: *conf4,
V6: V6ServerConf{}, V6: V6ServerConf{},
Leases: []*Lease{}, Leases: []*leaseDynamic{},
StaticLeases: []*Lease{}, StaticLeases: []*leaseStatic{},
Enabled: true, Enabled: true,
} }
@@ -95,7 +94,7 @@ func TestServer_handleDHCPStatus(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
resp := defaultResponse() resp := defaultResponse()
resp.StaticLeases = []*Lease{staticLease} resp.StaticLeases = []*leaseStatic{staticLease}
checkStatus(t, resp) checkStatus(t, resp)
}) })
@@ -106,7 +105,7 @@ func TestServer_handleDHCPStatus(t *testing.T) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(&Lease{}) err = json.NewEncoder(b).Encode(&leaseStatic{})
require.NoError(t, err) require.NoError(t, err)
var r *http.Request var r *http.Request

View File

@@ -20,7 +20,6 @@ import (
"github.com/go-ping/ping" "github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4" "github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/packet"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@@ -128,7 +127,7 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
s.leases = nil s.leases = nil
for _, l := range leases { for _, l := range leases {
if !l.IsStatic() { if !l.IsStatic {
l.Hostname = s.validHostnameForClient(l.Hostname, l.IP) l.Hostname = s.validHostnameForClient(l.Hostname, l.IP)
} }
err = s.addLease(l) err = s.addLease(l)
@@ -190,7 +189,7 @@ func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
continue continue
} }
if getStatic && l.IsStatic() { if getStatic && l.IsStatic {
leases = append(leases, l.Clone()) leases = append(leases, l.Clone())
} }
} }
@@ -211,7 +210,7 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
for _, l := range s.leases { for _, l := range s.leases {
if l.IP == ip { if l.IP == ip {
if l.Expiry.After(now) || l.IsStatic() { if l.IsStatic || l.Expiry.After(now) {
return l.HWAddr return l.HWAddr
} }
} }
@@ -259,7 +258,7 @@ func (s *v4Server) rmLeaseByIndex(i int) {
// Return error if a static lease is found // Return error if a static lease is found
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) { func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
for i, l := range s.leases { for i, l := range s.leases {
isStatic := l.IsStatic() isStatic := l.IsStatic
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP { if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP {
if isStatic { if isStatic {
@@ -292,7 +291,7 @@ func (s *v4Server) addLease(l *Lease) (err error) {
leaseIP := net.IP(l.IP.AsSlice()) leaseIP := net.IP(l.IP.AsSlice())
offset, inOffset := r.offset(leaseIP) offset, inOffset := r.offset(leaseIP)
if l.IsStatic() { if l.IsStatic {
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is // TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
// disabled. // disabled.
if sn := s.conf.subnet; !sn.Contains(l.IP) { if sn := s.conf.subnet; !sn.Contains(l.IP) {
@@ -359,6 +358,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
} }
l.Expiry = time.Unix(leaseExpireStatic, 0) l.Expiry = time.Unix(leaseExpireStatic, 0)
l.IsStatic = true
err = netutil.ValidateMAC(l.HWAddr) err = netutil.ValidateMAC(l.HWAddr)
if err != nil { if err != nil {
@@ -528,7 +528,7 @@ func (s *v4Server) nextIP() (ip net.IP) {
func (s *v4Server) findExpiredLease() int { func (s *v4Server) findExpiredLease() int {
now := time.Now() now := time.Now()
for i, lease := range s.leases { for i, lease := range s.leases {
if !lease.IsStatic() && lease.Expiry.Before(now) { if !lease.IsStatic && lease.Expiry.Before(now) {
return i return i
} }
} }
@@ -860,7 +860,7 @@ func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsR
s.leasesLock.Lock() s.leasesLock.Lock()
defer s.leasesLock.Unlock() defer s.leasesLock.Unlock()
if lease.IsStatic() { if lease.IsStatic {
if lease.Hostname != "" { if lease.Hostname != "" {
// TODO(e.burkov): This option is used to update the server's DNS // TODO(e.burkov): This option is used to update the server's DNS
// mapping. The option should only be answered when it has been // mapping. The option should only be answered when it has been
@@ -1131,56 +1131,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
s.send(peer, conn, req, resp) s.send(peer, conn, req, resp)
} }
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}
// Start starts the IPv4 DHCP server. // Start starts the IPv4 DHCP server.
func (s *v4Server) Start() (err error) { func (s *v4Server) Start() (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()

View File

@@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/packet"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -73,6 +72,7 @@ func TestV4Server_leasing(t *testing.T) {
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
IsStatic: true,
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -82,6 +82,7 @@ func TestV4Server_leasing(t *testing.T) {
Hostname: staticName, Hostname: staticName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: anotherIP, IP: anotherIP,
IsStatic: true,
}) })
assert.ErrorIs(t, err, ErrDupHostname) assert.ErrorIs(t, err, ErrDupHostname)
}) })
@@ -96,6 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
Hostname: anotherName, Hostname: anotherName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: anotherIP, IP: anotherIP,
IsStatic: true,
}) })
testutil.AssertErrorMsg(t, wantErrMsg, err) testutil.AssertErrorMsg(t, wantErrMsg, err)
}) })
@@ -110,6 +112,7 @@ func TestV4Server_leasing(t *testing.T) {
Hostname: anotherName, Hostname: anotherName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: staticIP, IP: staticIP,
IsStatic: true,
}) })
testutil.AssertErrorMsg(t, wantErrMsg, err) testutil.AssertErrorMsg(t, wantErrMsg, err)
}) })
@@ -326,7 +329,7 @@ func TestV4_AddReplace(t *testing.T) {
for i, l := range ls { for i, l := range ls {
assert.Equal(t, stLeases[i].IP, l.IP) assert.Equal(t, stLeases[i].IP, l.IP)
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
assert.True(t, l.IsStatic()) assert.True(t, l.IsStatic)
} }
} }
@@ -767,111 +770,6 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return fc.writeTo(p, addr) return fc.writeTo(p, addr)
} }
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}
func TestV4Server_FindMACbyIP(t *testing.T) { func TestV4Server_FindMACbyIP(t *testing.T) {
const ( const (
staticName = "static-client" staticName = "static-client"
@@ -890,6 +788,7 @@ func TestV4Server_FindMACbyIP(t *testing.T) {
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
IsStatic: true,
}, { }, {
Expiry: time.Unix(10, 0), Expiry: time.Unix(10, 0),
Hostname: anotherName, Hostname: anotherName,

View File

@@ -121,7 +121,7 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
for _, l := range s.leases { for _, l := range s.leases {
if l.IP == ip { if l.IP == ip {
if l.Expiry.After(now) || l.IsStatic() { if l.IsStatic || l.Expiry.After(now) {
return l.HWAddr return l.HWAddr
} }
} }

View File

@@ -331,6 +331,7 @@ func TestV6_FindMACbyIP(t *testing.T) {
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
IsStatic: true,
}, { }, {
Expiry: time.Unix(10, 0), Expiry: time.Unix(10, 0),
Hostname: anotherName, Hostname: anotherName,
@@ -344,6 +345,7 @@ func TestV6_FindMACbyIP(t *testing.T) {
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
IsStatic: true,
}, { }, {
Expiry: time.Unix(10, 0), Expiry: time.Unix(10, 0),
Hostname: anotherName, Hostname: anotherName,

View File

@@ -587,11 +587,11 @@ func (s *Server) prepareTLS(proxyConfig *proxy.Config) (err error) {
if s.conf.StrictSNICheck { if s.conf.StrictSNICheck {
if len(cert.DNSNames) != 0 { if len(cert.DNSNames) != 0 {
s.conf.dnsNames = cert.DNSNames s.conf.dnsNames = cert.DNSNames
log.Debug("dnsforward: using certificate's SAN as DNS names: %v", cert.DNSNames) log.Debug("dns: using certificate's SAN as DNS names: %v", cert.DNSNames)
slices.Sort(s.conf.dnsNames) slices.Sort(s.conf.dnsNames)
} else { } else {
s.conf.dnsNames = append(s.conf.dnsNames, cert.Subject.CommonName) s.conf.dnsNames = append(s.conf.dnsNames, cert.Subject.CommonName)
log.Debug("dnsforward: using certificate's CN as DNS name: %s", cert.Subject.CommonName) log.Debug("dns: using certificate's CN as DNS name: %s", cert.Subject.CommonName)
} }
} }
@@ -648,30 +648,46 @@ func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, er
// UpdatedProtectionStatus updates protection state, if the protection was // UpdatedProtectionStatus updates protection state, if the protection was
// disabled temporarily. Returns the updated state of protection. // disabled temporarily. Returns the updated state of protection.
func (s *Server) UpdatedProtectionStatus() (enabled bool) { func (s *Server) UpdatedProtectionStatus() (enabled bool, disabledUntil *time.Time) {
changed := false s.serverLock.RLock()
defer func() { defer s.serverLock.RUnlock()
if changed {
log.Info("dns: protection is restarted after pause") disabledUntil = s.conf.ProtectionDisabledUntil
s.conf.ConfigModified() if disabledUntil == nil {
} return s.conf.ProtectionEnabled, nil
}() }
if time.Now().Before(*disabledUntil) {
return false, disabledUntil
}
// Update the values in a separate goroutine, unless an update is already in
// progress. Since this method is called very often, and this update is a
// relatively rare situation, do not lock s.serverLock for writing, as that
// can lead to freezes.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/5661.
if s.protectionUpdateInProgress.CompareAndSwap(false, true) {
go s.enableProtectionAfterPause()
}
return true, nil
}
// enableProtectionAfterPause sets the protection configuration to enabled
// values. It is intended to be used as a goroutine.
func (s *Server) enableProtectionAfterPause() {
defer log.OnPanic("dns: enabling protection after pause")
defer s.protectionUpdateInProgress.Store(false)
defer s.conf.ConfigModified()
s.serverLock.Lock() s.serverLock.Lock()
defer s.serverLock.Unlock() defer s.serverLock.Unlock()
disabledUntil := s.conf.ProtectionDisabledUntil
if disabledUntil == nil {
return s.conf.ProtectionEnabled
}
if time.Now().Before(*disabledUntil) {
return false
}
s.conf.ProtectionEnabled = true s.conf.ProtectionEnabled = true
s.conf.ProtectionDisabledUntil = nil s.conf.ProtectionDisabledUntil = nil
changed = true
return true log.Info("dns: protection is restarted after pause")
} }

View File

@@ -22,7 +22,7 @@ import (
// To transfer information between modules // To transfer information between modules
// //
// TODO(s.chzhen): Add lowercased, non-FQDN version of the hostname from the // TODO(s.chzhen): Add lowercased, non-FQDN version of the hostname from the
// question of the request. // question of the request. Add persistent client.
type dnsContext struct { type dnsContext struct {
proxyCtx *proxy.DNSContext proxyCtx *proxy.DNSContext
@@ -206,7 +206,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
dctx.clientID = string(s.clientIDCache.Get(key[:])) dctx.clientID = string(s.clientIDCache.Get(key[:]))
// Get the client-specific filtering settings. // Get the client-specific filtering settings.
dctx.protectionEnabled = s.UpdatedProtectionStatus() dctx.protectionEnabled, _ = s.UpdatedProtectionStatus()
dctx.setts = s.getClientRequestFilteringSettings(dctx) dctx.setts = s.getClientRequestFilteringSettings(dctx)
return resultCodeSuccess return resultCodeSuccess
@@ -460,7 +460,7 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
} }
// indexFirstV4Label returns the index at which the reversed IPv4 address // indexFirstV4Label returns the index at which the reversed IPv4 address
// starts, assuiming the domain is pre-validated ARPA domain having in-addr and // starts, assuming the domain is pre-validated ARPA domain having in-addr and
// arpa labels removed. // arpa labels removed.
func indexFirstV4Label(domain string) (idx int) { func indexFirstV4Label(domain string) (idx int) {
idx = len(domain) idx = len(domain)
@@ -478,7 +478,7 @@ func indexFirstV4Label(domain string) (idx int) {
} }
// indexFirstV6Label returns the index at which the reversed IPv6 address // indexFirstV6Label returns the index at which the reversed IPv6 address
// starts, assuiming the domain is pre-validated ARPA domain having ip6 and arpa // starts, assuming the domain is pre-validated ARPA domain having ip6 and arpa
// labels removed. // labels removed.
func indexFirstV6Label(domain string) (idx int) { func indexFirstV6Label(domain string) (idx int) {
idx = len(domain) idx = len(domain)

View File

@@ -9,6 +9,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
@@ -111,6 +112,10 @@ type Server struct {
isRunning bool isRunning bool
// protectionUpdateInProgress is used to make sure that only one goroutine
// updating the protection configuration after a pause is running at a time.
protectionUpdateInProgress atomic.Bool
conf ServerConfig conf ServerConfig
// serverLock protects Server. // serverLock protects Server.
serverLock sync.RWMutex serverLock sync.RWMutex

View File

@@ -453,8 +453,9 @@ func TestSafeSearch(t *testing.T) {
SafeSearchCacheSize: 1000, SafeSearchCacheSize: 1000,
CacheTime: 30, CacheTime: 30,
} }
safeSearch, err := safesearch.NewDefaultSafeSearch( safeSearch, err := safesearch.NewDefault(
safeSearchConf, safeSearchConf,
"",
filterConf.SafeSearchCacheSize, filterConf.SafeSearchCacheSize,
time.Minute*time.Duration(filterConf.CacheTime), time.Minute*time.Duration(filterConf.CacheTime),
) )

View File

@@ -101,7 +101,7 @@ type jsonDNSConfig struct {
} }
func (s *Server) getDNSConfig() (c *jsonDNSConfig) { func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
protectionEnabled := s.UpdatedProtectionStatus() protectionEnabled, protectionDisabledUntil := s.UpdatedProtectionStatus()
s.serverLock.RLock() s.serverLock.RLock()
defer s.serverLock.RUnlock() defer s.serverLock.RUnlock()
@@ -128,12 +128,6 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
usePrivateRDNS := s.conf.UsePrivateRDNS usePrivateRDNS := s.conf.UsePrivateRDNS
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers) localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
var disabledUntil *time.Time
if s.conf.ProtectionDisabledUntil != nil {
t := *s.conf.ProtectionDisabledUntil
disabledUntil = &t
}
var upstreamMode string var upstreamMode string
if s.conf.FastestAddr { if s.conf.FastestAddr {
upstreamMode = "fastest_addr" upstreamMode = "fastest_addr"
@@ -169,7 +163,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
UsePrivateRDNS: &usePrivateRDNS, UsePrivateRDNS: &usePrivateRDNS,
LocalPTRUpstreams: &localPTRUpstreams, LocalPTRUpstreams: &localPTRUpstreams,
DefaultLocalPTRUpstreams: defLocalPTRUps, DefaultLocalPTRUpstreams: defLocalPTRUps,
DisabledUntil: disabledUntil, DisabledUntil: protectionDisabledUntil,
} }
} }

View File

@@ -18,6 +18,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns" "github.com/miekg/dns"
@@ -122,7 +123,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
s.conf = tc.conf() s.conf = tc.conf()
s.handleGetConfig(w, nil) s.handleGetConfig(w, nil)
cType := w.Header().Get(aghhttp.HdrNameContentType) cType := w.Header().Get(httphdr.ContentType)
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType) assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
assert.JSONEq(t, string(caseWant), w.Body.String()) assert.JSONEq(t, string(caseWant), w.Body.String())
}) })

View File

@@ -40,12 +40,17 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
log.Debug("client ip: %s", ip) log.Debug("client ip: %s", ip)
ipStr := ip.String()
ids := []string{ipStr, dctx.clientID}
// Synchronize access to s.queryLog and s.stats so they won't be suddenly // Synchronize access to s.queryLog and s.stats so they won't be suddenly
// uninitialized while in use. This can happen after proxy server has been // uninitialized while in use. This can happen after proxy server has been
// stopped, but its workers haven't yet exited. // stopped, but its workers haven't yet exited.
if shouldLog && if shouldLog &&
s.queryLog != nil && s.queryLog != nil &&
s.queryLog.ShouldLog(host, q.Qtype, q.Qclass) { // TODO(s.chzhen): Use dnsforward.dnsContext when it will start
// containing persistent client.
s.queryLog.ShouldLog(host, q.Qtype, q.Qclass, ids) {
s.logQuery(dctx, pctx, elapsed, ip) s.logQuery(dctx, pctx, elapsed, ip)
} else { } else {
log.Debug( log.Debug(
@@ -56,8 +61,11 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
) )
} }
if s.stats != nil && s.stats.ShouldCount(host, q.Qtype, q.Qclass) { if s.stats != nil &&
s.updateStats(dctx, elapsed, *dctx.result, ip) // TODO(s.chzhen): Use dnsforward.dnsContext when it will start
// containing persistent client.
s.stats.ShouldCount(host, q.Qtype, q.Qclass, ids) {
s.updateStats(dctx, elapsed, *dctx.result, ipStr)
} }
return resultCodeSuccess return resultCodeSuccess
@@ -110,7 +118,7 @@ func (s *Server) updateStats(
ctx *dnsContext, ctx *dnsContext,
elapsed time.Duration, elapsed time.Duration,
res filtering.Result, res filtering.Result,
clientIP net.IP, clientIP string,
) { ) {
pctx := ctx.proxyCtx pctx := ctx.proxyCtx
e := stats.Entry{} e := stats.Entry{}
@@ -119,8 +127,8 @@ func (s *Server) updateStats(
if clientID := ctx.clientID; clientID != "" { if clientID := ctx.clientID; clientID != "" {
e.Client = clientID e.Client = clientID
} else if clientIP != nil { } else {
e.Client = clientIP.String() e.Client = clientIP
} }
e.Time = uint32(elapsed / 1000) e.Time = uint32(elapsed / 1000)

View File

@@ -31,7 +31,7 @@ func (l *testQueryLog) Add(p *querylog.AddParams) {
} }
// ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog. // ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog.
func (l *testQueryLog) ShouldLog(string, uint16, uint16) bool { func (l *testQueryLog) ShouldLog(string, uint16, uint16, []string) bool {
return true return true
} }
@@ -50,7 +50,7 @@ func (l *testStats) Update(e stats.Entry) {
} }
// ShouldCount implements the [stats.Interface] interface for *testStats. // ShouldCount implements the [stats.Interface] interface for *testStats.
func (l *testStats) ShouldCount(string, uint16, uint16) bool { func (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {
return true return true
} }

View File

@@ -1,17 +1,17 @@
package filtering package filtering
import ( import "github.com/miekg/dns"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
)
// SafeSearch interface describes a service for search engines hosts rewrites. // SafeSearch interface describes a service for search engines hosts rewrites.
type SafeSearch interface { type SafeSearch interface {
// SearchHost returns a replacement address for the search engine host. // CheckHost checks host with safe search filter. CheckHost must be safe
SearchHost(host string, qtype uint16) (res *rules.DNSRewrite) // for concurrent use. qtype must be either [dns.TypeA] or [dns.TypeAAAA].
// CheckHost checks host with safe search engine.
CheckHost(host string, qtype uint16) (res Result, err error) CheckHost(host string, qtype uint16) (res Result, err error)
// Update updates the configuration of the safe search filter. Update must
// be safe for concurrent use. An implementation of Update may ignore some
// fields, but it must document which.
Update(conf SafeSearchConfig) (err error)
} }
// SafeSearchConfig is a struct with safe search related settings. // SafeSearchConfig is a struct with safe search related settings.
@@ -37,10 +37,12 @@ type SafeSearchConfig struct {
// [hostChecker.check]. // [hostChecker.check].
func (d *DNSFilter) checkSafeSearch( func (d *DNSFilter) checkSafeSearch(
host string, host string,
_ uint16, qtype uint16,
setts *Settings, setts *Settings,
) (res Result, err error) { ) (res Result, err error) {
if !setts.ProtectionEnabled || !setts.SafeSearchEnabled { if !setts.ProtectionEnabled ||
!setts.SafeSearchEnabled ||
(qtype != dns.TypeA && qtype != dns.TypeAAAA) {
return Result{}, nil return Result{}, nil
} }
@@ -50,8 +52,8 @@ func (d *DNSFilter) checkSafeSearch(
clientSafeSearch := setts.ClientSafeSearch clientSafeSearch := setts.ClientSafeSearch
if clientSafeSearch != nil { if clientSafeSearch != nil {
return clientSafeSearch.CheckHost(host, dns.TypeA) return clientSafeSearch.CheckHost(host, qtype)
} }
return d.safeSearch.CheckHost(host, dns.TypeA) return d.safeSearch.CheckHost(host, qtype)
} }

View File

@@ -1 +1 @@
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com |www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com

View File

@@ -1,3 +1,3 @@
|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com |duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com |start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com |www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com

View File

@@ -188,4 +188,4 @@
|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com |www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com |www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com |www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com |www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com

View File

@@ -1 +1 @@
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com |pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com

View File

@@ -49,4 +49,4 @@
|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56 |yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56 |yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56 |yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56 |yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56

View File

@@ -2,4 +2,4 @@
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com |m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com |youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com |youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com |www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com

View File

@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
@@ -53,44 +54,85 @@ func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool)
} }
} }
// DefaultSafeSearch is the default safesearch struct. // Default is the default safe search filter that uses filtering rules with the
type DefaultSafeSearch struct { // dnsrewrite modifier.
engine *urlfilter.DNSEngine type Default struct {
safeSearchCache cache.Cache // mu protects engine.
resolver filtering.Resolver mu *sync.RWMutex
cacheTime time.Duration
// engine is the filtering engine that contains the DNS rewrite rules.
// engine may be nil, which means that this safe search filter is disabled.
engine *urlfilter.DNSEngine
cache cache.Cache
resolver filtering.Resolver
logPrefix string
cacheTTL time.Duration
} }
// NewDefaultSafeSearch returns new safesearch struct. CacheTime is an element // NewDefault returns an initialized default safe search filter. name is used
// TTL (in minutes). // for logging.
func NewDefaultSafeSearch( func NewDefault(
conf filtering.SafeSearchConfig, conf filtering.SafeSearchConfig,
name string,
cacheSize uint, cacheSize uint,
cacheTime time.Duration, cacheTTL time.Duration,
) (ss *DefaultSafeSearch, err error) { ) (ss *Default, err error) {
engine, err := newEngine(filtering.SafeSearchListID, conf)
if err != nil {
return nil, err
}
var resolver filtering.Resolver = net.DefaultResolver var resolver filtering.Resolver = net.DefaultResolver
if conf.CustomResolver != nil { if conf.CustomResolver != nil {
resolver = conf.CustomResolver resolver = conf.CustomResolver
} }
return &DefaultSafeSearch{ ss = &Default{
engine: engine, mu: &sync.RWMutex{},
safeSearchCache: cache.New(cache.Config{
cache: cache.New(cache.Config{
EnableLRU: true, EnableLRU: true,
MaxSize: cacheSize, MaxSize: cacheSize,
}), }),
cacheTime: cacheTime, resolver: resolver,
resolver: resolver, // Use %s, because the client safe-search names already contain double
}, nil // quotes.
logPrefix: fmt.Sprintf("safesearch %s: ", name),
cacheTTL: cacheTTL,
}
err = ss.resetEngine(filtering.SafeSearchListID, conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return ss, nil
} }
// newEngine creates new engine for provided safe search configuration. // log is a helper for logging that includes the name of the safe search
func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.DNSEngine, err error) { // filter. level must be one of [log.DEBUG], [log.INFO], and [log.ERROR].
func (ss *Default) log(level log.Level, msg string, args ...any) {
switch level {
case log.DEBUG:
log.Debug(ss.logPrefix+msg, args...)
case log.INFO:
log.Info(ss.logPrefix+msg, args...)
case log.ERROR:
log.Error(ss.logPrefix+msg, args...)
default:
panic(fmt.Errorf("safesearch: unsupported logging level %d", level))
}
}
// resetEngine creates new engine for provided safe search configuration and
// sets it in ss.
func (ss *Default) resetEngine(
listID int,
conf filtering.SafeSearchConfig,
) (err error) {
if !conf.Enabled {
ss.log(log.INFO, "disabled")
return nil
}
var sb strings.Builder var sb strings.Builder
for service, serviceRules := range safeSearchRules { for service, serviceRules := range safeSearchRules {
if isServiceProtected(conf, service) { if isServiceProtected(conf, service) {
@@ -106,20 +148,73 @@ func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.D
rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList}) rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList})
if err != nil { if err != nil {
return nil, fmt.Errorf("creating rule storage: %w", err) return fmt.Errorf("creating rule storage: %w", err)
} }
engine = urlfilter.NewDNSEngine(rs) ss.engine = urlfilter.NewDNSEngine(rs)
log.Info("safesearch: filter %d: reset %d rules", listID, engine.RulesCount)
return engine, nil ss.log(log.INFO, "reset %d rules", ss.engine.RulesCount)
return nil
} }
// type check // type check
var _ filtering.SafeSearch = (*DefaultSafeSearch)(nil) var _ filtering.SafeSearch = (*Default)(nil)
// CheckHost implements the [filtering.SafeSearch] interface for
// *DefaultSafeSearch.
func (ss *Default) CheckHost(
host string,
qtype rules.RRType,
) (res filtering.Result, err error) {
start := time.Now()
defer func() {
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
}()
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
}
// Check cache. Return cached result if it was found
cachedValue, isFound := ss.getCachedResult(host, qtype)
if isFound {
ss.log(log.DEBUG, "found in cache: %q", host)
return cachedValue, nil
}
rewrite := ss.searchHost(host, qtype)
if rewrite == nil {
return filtering.Result{}, nil
}
fltRes, err := ss.newResult(rewrite, qtype)
if err != nil {
ss.log(log.DEBUG, "looking up addresses for %q: %s", host, err)
return filtering.Result{}, err
}
if fltRes != nil {
res = *fltRes
ss.setCacheResult(host, qtype, res)
return res, nil
}
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
}
// searchHost looks up DNS rewrites in the internal DNS filtering engine.
func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) {
ss.mu.RLock()
defer ss.mu.RUnlock()
if ss.engine == nil {
return nil
}
// SearchHost implements the [filtering.SafeSearch] interface for *DefaultSafeSearch.
func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.DNSRewrite) {
r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{ r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{
Hostname: strings.ToLower(host), Hostname: strings.ToLower(host),
DNSType: qtype, DNSType: qtype,
@@ -133,51 +228,11 @@ func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.D
return nil return nil
} }
// CheckHost implements the [filtering.SafeSearch] interface for // newResult creates Result object from rewrite rule. qtype must be either
// *DefaultSafeSearch. // [dns.TypeA] or [dns.TypeAAAA].
func (ss *DefaultSafeSearch) CheckHost( func (ss *Default) newResult(
host string,
qtype uint16,
) (res filtering.Result, err error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("safesearch: lookup for %s", host)
}
// Check cache. Return cached result if it was found
cachedValue, isFound := ss.getCachedResult(host)
if isFound {
log.Debug("safesearch: found in cache: %s", host)
return cachedValue, nil
}
rewrite := ss.SearchHost(host, qtype)
if rewrite == nil {
return filtering.Result{}, nil
}
dRes, err := ss.newResult(rewrite, qtype)
if err != nil {
log.Debug("safesearch: failed to lookup addresses for %s: %s", host, err)
return filtering.Result{}, err
}
if dRes != nil {
res = *dRes
ss.setCacheResult(host, res)
return res, nil
}
return filtering.Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", host)
}
// newResult creates Result object from rewrite rule.
func (ss *DefaultSafeSearch) newResult(
rewrite *rules.DNSRewrite, rewrite *rules.DNSRewrite,
qtype uint16, qtype rules.RRType,
) (res *filtering.Result, err error) { ) (res *filtering.Result, err error) {
res = &filtering.Result{ res = &filtering.Result{
Rules: []*filtering.ResultRule{{ Rules: []*filtering.ResultRule{{
@@ -187,7 +242,7 @@ func (ss *DefaultSafeSearch) newResult(
IsFiltered: true, IsFiltered: true,
} }
if rewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) { if rewrite.RRType == qtype {
ip, ok := rewrite.Value.(net.IP) ip, ok := rewrite.Value.(net.IP)
if !ok || ip == nil { if !ok || ip == nil {
return nil, nil return nil, nil
@@ -198,17 +253,25 @@ func (ss *DefaultSafeSearch) newResult(
return res, nil return res, nil
} }
if rewrite.NewCNAME == "" { host := rewrite.NewCNAME
if host == "" {
return nil, nil return nil, nil
} }
ips, err := ss.resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME) ss.log(log.DEBUG, "resolving %q", host)
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ss.log(log.DEBUG, "resolved %s", ips)
for _, ip := range ips { for _, ip := range ips {
if ip = ip.To4(); ip == nil { // TODO(a.garipov): Remove this filtering once the resolver we use
// actually learns about network.
ip = fitToProto(ip, qtype)
if ip == nil {
continue continue
} }
@@ -220,38 +283,71 @@ func (ss *DefaultSafeSearch) newResult(
return nil, nil return nil, nil
} }
// setCacheResult stores data in cache for host. // qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
func (ss *DefaultSafeSearch) setCacheResult(host string, res filtering.Result) { // It panics for other types.
expire := uint32(time.Now().Add(ss.cacheTime).Unix()) func qtypeToProto(qtype rules.RRType) (proto string) {
switch qtype {
case dns.TypeA:
return "ip4"
case dns.TypeAAAA:
return "ip6"
default:
panic(fmt.Errorf("safesearch: unsupported question type %s", dns.Type(qtype)))
}
}
// fitToProto returns a non-nil IP address if ip is the correct protocol version
// for qtype. qtype is expected to be either [dns.TypeA] or [dns.TypeAAAA].
func fitToProto(ip net.IP, qtype rules.RRType) (res net.IP) {
ip4 := ip.To4()
if qtype == dns.TypeA {
return ip4
}
if ip4 == nil {
return ip
}
return nil
}
// setCacheResult stores data in cache for host. qtype is expected to be either
// [dns.TypeA] or [dns.TypeAAAA].
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
expire := uint32(time.Now().Add(ss.cacheTTL).Unix())
exp := make([]byte, 4) exp := make([]byte, 4)
binary.BigEndian.PutUint32(exp, expire) binary.BigEndian.PutUint32(exp, expire)
buf := bytes.NewBuffer(exp) buf := bytes.NewBuffer(exp)
err := gob.NewEncoder(buf).Encode(res) err := gob.NewEncoder(buf).Encode(res)
if err != nil { if err != nil {
log.Error("safesearch: cache encoding: %s", err) ss.log(log.ERROR, "cache encoding: %s", err)
return return
} }
val := buf.Bytes() val := buf.Bytes()
_ = ss.safeSearchCache.Set([]byte(host), val) _ = ss.cache.Set([]byte(dns.Type(qtype).String()+" "+host), val)
log.Debug("safesearch: stored in cache: %s (%d bytes)", host, len(val)) ss.log(log.DEBUG, "stored in cache: %q, %d bytes", host, len(val))
} }
// getCachedResult returns stored data from cache for host. // getCachedResult returns stored data from cache for host. qtype is expected
func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result, ok bool) { // to be either [dns.TypeA] or [dns.TypeAAAA].
func (ss *Default) getCachedResult(
host string,
qtype rules.RRType,
) (res filtering.Result, ok bool) {
res = filtering.Result{} res = filtering.Result{}
data := ss.safeSearchCache.Get([]byte(host)) data := ss.cache.Get([]byte(dns.Type(qtype).String() + " " + host))
if data == nil { if data == nil {
return res, false return res, false
} }
exp := binary.BigEndian.Uint32(data[:4]) exp := binary.BigEndian.Uint32(data[:4])
if exp <= uint32(time.Now().Unix()) { if exp <= uint32(time.Now().Unix()) {
ss.safeSearchCache.Del([]byte(host)) ss.cache.Del([]byte(host))
return res, false return res, false
} }
@@ -260,10 +356,27 @@ func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result,
err := gob.NewDecoder(buf).Decode(&res) err := gob.NewDecoder(buf).Decode(&res)
if err != nil { if err != nil {
log.Debug("safesearch: cache decoding: %s", err) ss.log(log.ERROR, "cache decoding: %s", err)
return filtering.Result{}, false return filtering.Result{}, false
} }
return res, true return res, true
} }
// Update implements the [filtering.SafeSearch] interface for *Default. Update
// ignores the CustomResolver and Enabled fields.
func (ss *Default) Update(conf filtering.SafeSearchConfig) (err error) {
ss.mu.Lock()
defer ss.mu.Unlock()
err = ss.resetEngine(filtering.SafeSearchListID, conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
ss.cache.Clear()
return nil
}

View File

@@ -0,0 +1,137 @@
package safesearch
import (
"context"
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO(a.garipov): Move as much of this as possible into proper external tests.
const (
// TODO(a.garipov): Add IPv6 tests.
testQType = dns.TypeA
testCacheSize = 5000
testCacheTTL = 30 * time.Minute
)
var defaultSafeSearchConf = filtering.SafeSearchConfig{
Enabled: true,
Bing: true,
DuckDuckGo: true,
Google: true,
Pixabay: true,
Yandex: true,
YouTube: true,
}
var yandexIP = net.IPv4(213, 180, 193, 56)
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *Default) {
ss, err := NewDefault(ssConf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
return ss
}
func TestSafeSearch(t *testing.T) {
ss := newForTest(t, defaultSafeSearchConf)
val := ss.searchHost("www.google.com", testQType)
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
}
func TestSafeSearchCacheYandex(t *testing.T) {
const domain = "yandex.ru"
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
// Check host with disabled safesearch.
res, err := ss.CheckHost(domain, testQType)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
assert.Empty(t, res.Rules)
ss = newForTest(t, defaultSafeSearchConf)
res, err = ss.CheckHost(domain, testQType)
require.NoError(t, err)
// For yandex we already know valid IP.
require.Len(t, res.Rules, 1)
assert.Equal(t, res.Rules[0].IP, yandexIP)
// Check cache.
cachedValue, isFound := ss.getCachedResult(domain, testQType)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
}
func TestSafeSearchCacheGoogle(t *testing.T) {
const domain = "www.google.ru"
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
res, err := ss.CheckHost(domain, testQType)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
assert.Empty(t, res.Rules)
resolver := &aghtest.TestResolver{}
ss = newForTest(t, defaultSafeSearchConf)
ss.resolver = resolver
// Lookup for safesearch domain.
rewrite := ss.searchHost(domain, testQType)
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
require.NoError(t, err)
var foundIP net.IP
for _, ip := range ips {
if ip.To4() != nil {
foundIP = ip
break
}
}
res, err = ss.CheckHost(domain, testQType)
require.NoError(t, err)
require.Len(t, res.Rules, 1)
assert.True(t, res.Rules[0].IP.Equal(foundIP))
// Check cache.
cachedValue, isFound := ss.getCachedResult(domain, testQType)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
}
const googleHost = "www.google.com"
var dnsRewriteSink *rules.DNSRewrite
func BenchmarkSafeSearch(b *testing.B) {
ss := newForTest(b, defaultSafeSearchConf)
for n := 0; n < b.N; n++ {
dnsRewriteSink = ss.searchHost(googleHost, testQType)
}
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
}

View File

@@ -1,26 +1,37 @@
package safesearch package safesearch_test
import ( import (
"context"
"net" "net"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// Common test constants.
const ( const (
safeSearchCacheSize = 5000 // TODO(a.garipov): Add IPv6 tests.
cacheTime = 30 * time.Minute testQType = dns.TypeA
testCacheSize = 5000
testCacheTTL = 30 * time.Minute
) )
var defaultSafeSearchConf = filtering.SafeSearchConfig{ // testConf is the default safe search configuration for tests.
Enabled: true, var testConf = filtering.SafeSearchConfig{
CustomResolver: nil,
Enabled: true,
Bing: true, Bing: true,
DuckDuckGo: true, DuckDuckGo: true,
Google: true, Google: true,
@@ -29,25 +40,15 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
YouTube: true, YouTube: true,
} }
// yandexIP is the expected IP address of Yandex safe search results. Keep in
// sync with the rules data.
var yandexIP = net.IPv4(213, 180, 193, 56) var yandexIP = net.IPv4(213, 180, 193, 56)
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *DefaultSafeSearch) { func TestDefault_CheckHost_yandex(t *testing.T) {
ss, err := NewDefaultSafeSearch(ssConf, safeSearchCacheSize, cacheTime) conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err) require.NoError(t, err)
return ss
}
func TestSafeSearch(t *testing.T) {
ss := newForTest(t, defaultSafeSearchConf)
val := ss.SearchHost("www.google.com", dns.TypeA)
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
}
func TestCheckHostSafeSearchYandex(t *testing.T) {
ss := newForTest(t, defaultSafeSearchConf)
// Check host for each domain. // Check host for each domain.
for _, host := range []string{ for _, host := range []string{
"yandex.ru", "yandex.ru",
@@ -57,7 +58,8 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
"yandex.kz", "yandex.kz",
"www.yandex.com", "www.yandex.com",
} { } {
res, err := ss.CheckHost(host, dns.TypeA) var res filtering.Result
res, err = ss.CheckHost(host, testQType)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, res.IsFiltered) assert.True(t, res.IsFiltered)
@@ -69,12 +71,14 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
} }
} }
func TestCheckHostSafeSearchGoogle(t *testing.T) { func TestDefault_CheckHost_google(t *testing.T) {
resolver := &aghtest.TestResolver{} resolver := &aghtest.TestResolver{}
ip, _ := resolver.HostToIPs("forcesafesearch.google.com") ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
ss := newForTest(t, defaultSafeSearchConf) conf := testConf
ss.resolver = resolver conf.CustomResolver = resolver
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
// Check host for each domain. // Check host for each domain.
for _, host := range []string{ for _, host := range []string{
@@ -87,7 +91,8 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
"www.google.je", "www.google.je",
} { } {
t.Run(host, func(t *testing.T) { t.Run(host, func(t *testing.T) {
res, err := ss.CheckHost(host, dns.TypeA) var res filtering.Result
res, err = ss.CheckHost(host, testQType)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, res.IsFiltered) assert.True(t, res.IsFiltered)
@@ -100,103 +105,35 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
} }
} }
func TestSafeSearchCacheYandex(t *testing.T) { func TestDefault_Update(t *testing.T) {
const domain = "yandex.ru" conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
// Check host with disabled safesearch.
res, err := ss.CheckHost(domain, dns.TypeA)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, res.IsFiltered) res, err := ss.CheckHost("www.yandex.com", testQType)
assert.Empty(t, res.Rules)
ss = newForTest(t, defaultSafeSearchConf)
res, err = ss.CheckHost(domain, dns.TypeA)
require.NoError(t, err) require.NoError(t, err)
// For yandex we already know valid IP. assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, res.Rules[0].IP, yandexIP) err = ss.Update(filtering.SafeSearchConfig{
Enabled: true,
// Check cache. Google: false,
cachedValue, isFound := ss.getCachedResult(domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
}
func TestSafeSearchCacheGoogle(t *testing.T) {
const domain = "www.google.ru"
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
res, err := ss.CheckHost(domain, dns.TypeA)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
assert.Empty(t, res.Rules)
resolver := &aghtest.TestResolver{}
ss = newForTest(t, defaultSafeSearchConf)
ss.resolver = resolver
// Lookup for safesearch domain.
rewrite := ss.SearchHost(domain, dns.TypeA)
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
require.NoError(t, err)
var foundIP net.IP
for _, ip := range ips {
if ip.To4() != nil {
foundIP = ip
break
}
}
res, err = ss.CheckHost(domain, dns.TypeA)
require.NoError(t, err)
require.Len(t, res.Rules, 1)
assert.True(t, res.Rules[0].IP.Equal(foundIP))
// Check cache.
cachedValue, isFound := ss.getCachedResult(domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
}
const googleHost = "www.google.com"
var dnsRewriteSink *rules.DNSRewrite
func BenchmarkSafeSearch(b *testing.B) {
ss := newForTest(b, defaultSafeSearchConf)
for n := 0; n < b.N; n++ {
dnsRewriteSink = ss.SearchHost(googleHost, dns.TypeA)
}
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
}
var dnsRewriteParallelSink *rules.DNSRewrite
func BenchmarkSafeSearch_parallel(b *testing.B) {
ss := newForTest(b, defaultSafeSearchConf)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
dnsRewriteParallelSink = ss.SearchHost(googleHost, dns.TypeA)
}
}) })
require.NoError(t, err)
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteParallelSink.NewCNAME) res, err = ss.CheckHost("www.yandex.com", testQType)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
err = ss.Update(filtering.SafeSearchConfig{
Enabled: false,
Google: true,
})
require.NoError(t, err)
res, err = ss.CheckHost("www.yandex.com", testQType)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
} }

View File

@@ -50,11 +50,19 @@ func (d *DNSFilter) handleSafeSearchSettings(w http.ResponseWriter, r *http.Requ
return return
} }
conf := *req
err = d.safeSearch.Update(conf)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "updating: %s", err)
return
}
func() { func() {
d.confLock.Lock() d.confLock.Lock()
defer d.confLock.Unlock() defer d.confLock.Unlock()
d.Config.SafeSearchConf = *req d.Config.SafeSearchConf = conf
}() }()
d.Config.ConfigModified() d.Config.ConfigModified()

View File

@@ -311,6 +311,14 @@ var blockedServices = []blockedService{{
"||warp.plus^", "||warp.plus^",
"||workers.dev^", "||workers.dev^",
}, },
}, {
ID: "crunchyroll",
Name: "Crunchyroll",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M 25 3 C 12.85 3 3 12.85 3 25 C 3 40.188 13.387672 44.538609 20.388672 45.974609 C 20.427672 45.982609 20.465953 45.986328 20.501953 45.986328 C 21.006953 45.986328 21.206312 45.25525 20.695312 45.03125 C 13.285312 41.79025 8.0301562 34.327141 9.1601562 25.494141 C 10.256156 16.920141 17.244938 10.069141 25.835938 9.1191406 C 26.564937 9.0381406 27.287 9 28 9 C 35.541 9 42.044422 13.395672 45.107422 19.763672 C 45.206422 19.968672 45.382594 20.058594 45.558594 20.058594 C 45.853594 20.058594 46.144828 19.8075 46.048828 19.4375 C 44.302828 12.7105 39 3 25 3 z M 29 14 C 20.481 14 13.619625 21.101031 14.015625 29.707031 C 14.366625 37.346031 20.653016 43.631422 28.291016 43.982422 C 28.528016 43.994422 28.766 44 29 44 C 37.285 44 44 37.285 44 29 C 44 27.819 43.860563 26.670359 43.601562 25.568359 C 43.542563 25.319359 43.332234 25.183594 43.115234 25.183594 C 42.961234 25.183594 42.806266 25.251484 42.697266 25.396484 C 41.512266 26.976484 39.627 28 37.5 28 C 37.397 28 37.293453 27.997188 37.189453 27.992188 C 34.031453 27.845188 31.348203 25.317875 31.033203 22.171875 C 30.763203 19.477875 32.142297 17.082328 34.279297 15.861328 C 34.656297 15.646328 34.62475 15.100266 34.21875 14.947266 C 32.59375 14.340266 30.838 14 29 14 z M 44.296875 26.595703 L 44.300781 26.595703 L 44.296875 26.595703 z\"/></svg>"),
Rules: []string{
"||crunchyroll.com^",
"||gccrunchyroll.com^",
},
}, { }, {
ID: "dailymotion", ID: "dailymotion",
Name: "Dailymotion", Name: "Dailymotion",

View File

@@ -16,6 +16,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@@ -379,9 +380,9 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
// TODO(a.garipov): Support header Forwarded from RFC 7329. // TODO(a.garipov): Support header Forwarded from RFC 7329.
func realIP(r *http.Request) (ip net.IP, err error) { func realIP(r *http.Request) (ip net.IP, err error) {
proxyHeaders := []string{ proxyHeaders := []string{
"CF-Connecting-IP", httphdr.CFConnectingIP,
"True-Client-IP", httphdr.TrueClientIP,
"X-Real-IP", httphdr.XRealIP,
} }
for _, h := range proxyHeaders { for _, h := range proxyHeaders {
@@ -394,7 +395,7 @@ func realIP(r *http.Request) (ip net.IP, err error) {
// If none of the above yielded any results, get the leftmost IP address // If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header. // from the X-Forwarded-For header.
s := r.Header.Get("X-Forwarded-For") s := r.Header.Get(httphdr.XForwardedFor)
ipStrs := strings.SplitN(s, ", ", 2) ipStrs := strings.SplitN(s, ", ", 2)
ip = net.ParseIP(ipStrs[0]) ip = net.ParseIP(ipStrs[0])
if ip != nil { if ip != nil {
@@ -411,6 +412,21 @@ func realIP(r *http.Request) (ip net.IP, err error) {
return net.ParseIP(ipStr), nil return net.ParseIP(ipStr), nil
} }
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
// when it writes to the log.
func writeErrorWithIP(
r *http.Request,
w http.ResponseWriter,
code int,
remoteIP string,
format string,
args ...any,
) {
text := fmt.Sprintf(format, args...)
log.Error("%s %s %s: from ip %s: %s", r.Method, r.Host, r.URL, remoteIP, text)
http.Error(w, text, code)
}
func handleLogin(w http.ResponseWriter, r *http.Request) { func handleLogin(w http.ResponseWriter, r *http.Request) {
req := loginJSON{} req := loginJSON{}
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
@@ -420,31 +436,45 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
var remoteAddr string var remoteIP string
// realIP cannot be used here without taking TrustedProxies into account due // realIP cannot be used here without taking TrustedProxies into account due
// to security issues. // to security issues.
// //
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799. // See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
// //
// TODO(e.burkov): Use realIP when the issue will be fixed. // TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteAddr, err = netutil.SplitHost(r.RemoteAddr); err != nil { if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "auth: getting remote address: %s", err) writeErrorWithIP(
r,
w,
http.StatusBadRequest,
r.RemoteAddr,
"auth: getting remote address: %s",
err,
)
return return
} }
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil { if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteAddr); left > 0 { if left := rateLimiter.check(remoteIP); left > 0 {
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds()))) w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left) writeErrorWithIP(
r,
w,
http.StatusTooManyRequests,
remoteIP,
"auth: blocked for %s",
left,
)
return return
} }
} }
cookie, err := Context.auth.newCookie(req, remoteAddr) cookie, err := Context.auth.newCookie(req, remoteIP)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusForbidden, "%s", err) writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
return return
} }
@@ -452,10 +482,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
// Use realIP here, since this IP address is only used for logging. // Use realIP here, since this IP address is only used for logging.
ip, err := realIP(r) ip, err := realIP(r)
if err != nil { if err != nil {
log.Error("auth: getting real ip from request: %s", err) log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
} else if ip == nil {
// Technically shouldn't happen.
log.Error("auth: unknown ip")
} }
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip) log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
@@ -463,9 +490,9 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
h := w.Header() h := w.Header()
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate") h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set("Pragma", "no-cache") h.Set(httphdr.Pragma, "no-cache")
h.Set("Expires", "0") h.Set(httphdr.Expires, "0")
aghhttp.OK(w) aghhttp.OK(w)
} }
@@ -476,7 +503,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie]. // The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out. // The user is already logged out.
respHdr.Set("Location", "/login.html") respHdr.Set(httphdr.Location, "/login.html")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
return return
@@ -494,8 +521,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
} }
respHdr.Set("Location", "/login.html") respHdr.Set(httphdr.Location, "/login.html")
respHdr.Set("Set-Cookie", c.String()) respHdr.Set(httphdr.SetCookie, c.String())
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
} }
@@ -543,8 +570,7 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
log.Debug("auth: redirected to login page by GL-Inet submodule") log.Debug("auth: redirected to login page by GL-Inet submodule")
} else { } else {
log.Debug("auth: redirected to login page") log.Debug("auth: redirected to login page")
w.Header().Set("Location", "/login.html") http.Redirect(w, r, "login.html", http.StatusFound)
w.WriteHeader(http.StatusFound)
} }
} else { } else {
log.Debug("auth: responded with forbidden to %s %s", r.Method, p) log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
@@ -569,8 +595,7 @@ func optionalAuth(
// Redirect to the dashboard if already authenticated. // Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value) res := Context.auth.checkSession(cookie.Value)
if res == checkSessionOK { if res == checkSessionOK {
w.Header().Set("Location", "/") http.Redirect(w, r, "", http.StatusFound)
w.WriteHeader(http.StatusFound)
return return
} }

View File

@@ -12,6 +12,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -135,11 +136,11 @@ func TestAuthHTTP(t *testing.T) {
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.Equal(t, http.StatusFound, w.statusCode) assert.Equal(t, http.StatusFound, w.statusCode)
assert.NotEmpty(t, w.hdr.Get("Location")) assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
assert.False(t, handlerCalled) assert.False(t, handlerCalled)
// go to login page // go to login page
loginURL := w.hdr.Get("Location") loginURL := w.hdr.Get(httphdr.Location)
r.URL = &url.URL{Path: loginURL} r.URL = &url.URL{Path: loginURL}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
@@ -153,13 +154,13 @@ func TestAuthHTTP(t *testing.T) {
// get / // get /
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
w.hdr = make(http.Header) w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String()) r.Header.Set(httphdr.Cookie, cookie.String())
r.URL = &url.URL{Path: "/"} r.URL = &url.URL{Path: "/"}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del("Cookie") r.Header.Del(httphdr.Cookie)
// get / with basic auth // get / with basic auth
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
@@ -169,28 +170,28 @@ func TestAuthHTTP(t *testing.T) {
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del("Authorization") r.Header.Del(httphdr.Authorization)
// get login page with a valid cookie - we're redirected to / // get login page with a valid cookie - we're redirected to /
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
w.hdr = make(http.Header) w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String()) r.Header.Set(httphdr.Cookie, cookie.String())
r.URL = &url.URL{Path: loginURL} r.URL = &url.URL{Path: loginURL}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.NotEmpty(t, w.hdr.Get("Location")) assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
assert.False(t, handlerCalled) assert.False(t, handlerCalled)
r.Header.Del("Cookie") r.Header.Del(httphdr.Cookie)
// get login page with an invalid cookie // get login page with an invalid cookie
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
w.hdr = make(http.Header) w.hdr = make(http.Header)
r.Header.Set("Cookie", "bad") r.Header.Set(httphdr.Cookie, "bad")
r.URL = &url.URL{Path: loginURL} r.URL = &url.URL{Path: loginURL}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del("Cookie") r.Header.Del(httphdr.Cookie)
Context.auth.Close() Context.auth.Close()
} }
@@ -213,7 +214,7 @@ func TestRealIP(t *testing.T) {
}, { }, {
name: "success_proxy", name: "success_proxy",
header: http.Header{ header: http.Header{
textproto.CanonicalMIMEHeaderKey("X-Real-IP"): []string{"1.2.3.5"}, textproto.CanonicalMIMEHeaderKey(httphdr.XRealIP): []string{"1.2.3.5"},
}, },
remoteAddr: remoteAddr, remoteAddr: remoteAddr,
wantErrMsg: "", wantErrMsg: "",
@@ -221,7 +222,7 @@ func TestRealIP(t *testing.T) {
}, { }, {
name: "success_proxy_multiple", name: "success_proxy_multiple",
header: http.Header{ header: http.Header{
textproto.CanonicalMIMEHeaderKey("X-Forwarded-For"): []string{ textproto.CanonicalMIMEHeaderKey(httphdr.XForwardedFor): []string{
"1.2.3.6, 1.2.3.5", "1.2.3.6, 1.2.3.5",
}, },
}, },

View File

@@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/josharian/native"
) )
// GLMode - enable GL-Inet compatibility mode // GLMode - enable GL-Inet compatibility mode
@@ -102,7 +102,7 @@ func glGetTokenDate(file string) uint32 {
buf := bytes.NewBuffer(bs) buf := bytes.NewBuffer(bs)
err = binary.Read(buf, aghos.NativeEndian, &dateToken) err = binary.Read(buf, native.Endian, &dateToken)
if err != nil { if err != nil {
log.Error("decoding token: %s", err) log.Error("decoding token: %s", err)

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/josharian/native"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -19,13 +19,13 @@ func TestAuthGL(t *testing.T) {
glFilePrefix = dir + "/gl_token_" glFilePrefix = dir + "/gl_token_"
data := make([]byte, 4) data := make([]byte, 4)
aghos.NativeEndian.PutUint32(data, 1) native.Endian.PutUint32(data, 1)
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644)) require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
assert.False(t, glCheckToken("test")) assert.False(t, glCheckToken("test"))
data = make([]byte, 4) data = make([]byte, 4)
aghos.NativeEndian.PutUint32(data, uint32(time.Now().UTC().Unix()+60)) native.Endian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644)) require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil) r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil)

View File

@@ -3,8 +3,10 @@ package home
import ( import (
"encoding" "encoding"
"fmt" "fmt"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
) )
@@ -31,6 +33,8 @@ type Client struct {
SafeBrowsingEnabled bool SafeBrowsingEnabled bool
ParentalEnabled bool ParentalEnabled bool
UseOwnBlockedServices bool UseOwnBlockedServices bool
IgnoreQueryLog bool
IgnoreStatistics bool
} }
// closeUpstreams closes the client-specific upstream config of c if any. // closeUpstreams closes the client-specific upstream config of c if any.
@@ -45,6 +49,23 @@ func (c *Client) closeUpstreams() (err error) {
return nil return nil
} }
// setSafeSearch initializes and sets the safe search filter for this client.
func (c *Client) setSafeSearch(
conf filtering.SafeSearchConfig,
cacheSize uint,
cacheTTL time.Duration,
) (err error) {
ss, err := safesearch.NewDefault(conf, fmt.Sprintf("client %q", c.Name), cacheSize, cacheTTL)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
c.SafeSearch = ss
return nil
}
// clientSource represents the source from which the information about the // clientSource represents the source from which the information about the
// client has been obtained. // client has been obtained.
type clientSource uint type clientSource uint

View File

@@ -13,7 +13,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@@ -52,9 +51,17 @@ type clientsContainer struct {
// lock protects all fields. // lock protects all fields.
// //
// TODO(a.garipov): Use a pointer and describe which fields are protected in // TODO(a.garipov): Use a pointer and describe which fields are protected in
// more detail. // more detail. Use sync.RWMutex.
lock sync.Mutex lock sync.Mutex
// safeSearchCacheSize is the size of the safe search cache to use for
// persistent clients.
safeSearchCacheSize uint
// safeSearchCacheTTL is the TTL of the safe search cache to use for
// persistent clients.
safeSearchCacheTTL time.Duration
// testing is a flag that disables some features for internal tests. // testing is a flag that disables some features for internal tests.
// //
// TODO(a.garipov): Awful. Remove. // TODO(a.garipov): Awful. Remove.
@@ -74,6 +81,7 @@ func (clients *clientsContainer) Init(
if clients.list != nil { if clients.list != nil {
log.Fatal("clients.list != nil") log.Fatal("clients.list != nil")
} }
clients.list = make(map[string]*Client) clients.list = make(map[string]*Client)
clients.idIndex = make(map[string]*Client) clients.idIndex = make(map[string]*Client)
clients.ipToRC = map[netip.Addr]*RuntimeClient{} clients.ipToRC = map[netip.Addr]*RuntimeClient{}
@@ -85,6 +93,9 @@ func (clients *clientsContainer) Init(
clients.arpdb = arpdb clients.arpdb = arpdb
clients.addFromConfig(objects, filteringConf) clients.addFromConfig(objects, filteringConf)
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
if clients.testing { if clients.testing {
return return
} }
@@ -148,6 +159,9 @@ type clientObject struct {
ParentalEnabled bool `yaml:"parental_enabled"` ParentalEnabled bool `yaml:"parental_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"` SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
IgnoreQueryLog bool `yaml:"ignore_querylog"`
IgnoreStatistics bool `yaml:"ignore_statistics"`
} }
// addFromConfig initializes the clients container with objects from the // addFromConfig initializes the clients container with objects from the
@@ -166,23 +180,23 @@ func (clients *clientsContainer) addFromConfig(objects []*clientObject, filterin
safeSearchConf: o.SafeSearchConf, safeSearchConf: o.SafeSearchConf,
SafeBrowsingEnabled: o.SafeBrowsingEnabled, SafeBrowsingEnabled: o.SafeBrowsingEnabled,
UseOwnBlockedServices: !o.UseGlobalBlockedServices, UseOwnBlockedServices: !o.UseGlobalBlockedServices,
IgnoreQueryLog: o.IgnoreQueryLog,
IgnoreStatistics: o.IgnoreStatistics,
} }
if o.SafeSearchConf.Enabled { if o.SafeSearchConf.Enabled {
o.SafeSearchConf.CustomResolver = safeSearchResolver{} o.SafeSearchConf.CustomResolver = safeSearchResolver{}
ss, err := safesearch.NewDefaultSafeSearch( err := cli.setSafeSearch(
o.SafeSearchConf, o.SafeSearchConf,
filteringConf.SafeSearchCacheSize, filteringConf.SafeSearchCacheSize,
time.Minute*time.Duration(filteringConf.CacheTime), time.Minute*time.Duration(filteringConf.CacheTime),
) )
if err != nil { if err != nil {
log.Error("clients: init client safesearch %s: %s", cli.Name, err) log.Error("clients: init client safesearch %q: %s", cli.Name, err)
continue continue
} }
cli.SafeSearch = ss
} }
for _, s := range o.BlockedServices { for _, s := range o.BlockedServices {
@@ -232,6 +246,8 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
SafeSearchConf: cli.safeSearchConf, SafeSearchConf: cli.safeSearchConf,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled, SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
UseGlobalBlockedServices: !cli.UseOwnBlockedServices, UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
IgnoreQueryLog: cli.IgnoreQueryLog,
IgnoreStatistics: cli.IgnoreStatistics,
} }
objs = append(objs, o) objs = append(objs, o)
@@ -343,7 +359,8 @@ func (clients *clientsContainer) clientOrArtificial(
client, ok := clients.Find(id) client, ok := clients.Find(id)
if ok { if ok {
return &querylog.Client{ return &querylog.Client{
Name: client.Name, Name: client.Name,
IgnoreQueryLog: client.IgnoreQueryLog,
}, false }, false
} }
@@ -378,6 +395,20 @@ func (clients *clientsContainer) Find(id string) (c *Client, ok bool) {
return c, true return c, true
} }
// shouldCountClient is a wrapper around Find to make it a valid client
// information finder for the statistics. If no information about the client
// is found, it returns true.
func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
for _, id := range ids {
client, ok := clients.Find(id)
if ok {
return !client.IgnoreStatistics
}
}
return true
}
// findUpstreams returns upstreams configured for the client, identified either // findUpstreams returns upstreams configured for the client, identified either
// by its IP address or its ClientID. upsConf is nil if the client isn't found // by its IP address or its ClientID. upsConf is nil if the client isn't found
// or if the client has no custom upstreams. // or if the client has no custom upstreams.

View File

@@ -9,17 +9,27 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestClients(t *testing.T) { // newClientsContainer is a helper that creates a new clients container for
clients := clientsContainer{} // tests.
clients.testing = true func newClientsContainer() (c *clientsContainer) {
c = &clientsContainer{
testing: true,
}
clients.Init(nil, nil, nil, nil, nil) c.Init(nil, nil, nil, nil, &filtering.Config{})
return c
}
func TestClients(t *testing.T) {
clients := newClientsContainer()
t.Run("add_success", func(t *testing.T) { t.Run("add_success", func(t *testing.T) {
var ( var (
@@ -198,10 +208,7 @@ func TestClients(t *testing.T) {
} }
func TestClientsWHOIS(t *testing.T) { func TestClientsWHOIS(t *testing.T) {
clients := clientsContainer{ clients := newClientsContainer()
testing: true,
}
clients.Init(nil, nil, nil, nil, nil)
whois := &RuntimeClientWHOISInfo{ whois := &RuntimeClientWHOISInfo{
Country: "AU", Country: "AU",
Orgname: "Example Org", Orgname: "Example Org",
@@ -247,10 +254,7 @@ func TestClientsWHOIS(t *testing.T) {
} }
func TestClientsAddExisting(t *testing.T) { func TestClientsAddExisting(t *testing.T) {
clients := clientsContainer{ clients := newClientsContainer()
testing: true,
}
clients.Init(nil, nil, nil, nil, nil)
t.Run("simple", func(t *testing.T) { t.Run("simple", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1") ip := netip.MustParseAddr("1.1.1.1")
@@ -325,10 +329,7 @@ func TestClientsAddExisting(t *testing.T) {
} }
func TestClientsCustomUpstream(t *testing.T) { func TestClientsCustomUpstream(t *testing.T) {
clients := clientsContainer{ clients := newClientsContainer()
testing: true,
}
clients.Init(nil, nil, nil, nil, nil)
// Add client with upstreams. // Add client with upstreams.
ok, err := clients.Add(&Client{ ok, err := clients.Add(&Client{

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
) )
@@ -44,13 +45,16 @@ type clientJSON struct {
SafeSearchEnabled bool `json:"safesearch_enabled"` SafeSearchEnabled bool `json:"safesearch_enabled"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
UseGlobalSettings bool `json:"use_global_settings"` UseGlobalSettings bool `json:"use_global_settings"`
IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"`
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
} }
type runtimeClientJSON struct { type runtimeClientJSON struct {
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"` WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
Name string `json:"name"`
IP netip.Addr `json:"ip"` IP netip.Addr `json:"ip"`
Name string `json:"name"`
Source clientSource `json:"source"` Source clientSource `json:"source"`
} }
@@ -90,14 +94,16 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
} }
// jsonToClient converts JSON object to Client object. // jsonToClient converts JSON object to Client object.
func jsonToClient(cj clientJSON) (c *Client) { func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *Client, err error) {
var safeSearchConf filtering.SafeSearchConfig var safeSearchConf filtering.SafeSearchConfig
if cj.SafeSearchConf != nil { if cj.SafeSearchConf != nil {
safeSearchConf = *cj.SafeSearchConf safeSearchConf = *cj.SafeSearchConf
} else { } else {
// TODO(d.kolyshev): Remove after cleaning the deprecated // TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field. // [clientJSON.SafeSearchEnabled] field.
safeSearchConf = filtering.SafeSearchConfig{Enabled: cj.SafeSearchEnabled} safeSearchConf = filtering.SafeSearchConfig{
Enabled: cj.SafeSearchEnabled,
}
// Set default service flags for enabled safesearch. // Set default service flags for enabled safesearch.
if safeSearchConf.Enabled { if safeSearchConf.Enabled {
@@ -110,20 +116,47 @@ func jsonToClient(cj clientJSON) (c *Client) {
} }
} }
return &Client{ c = &Client{
Name: cj.Name, safeSearchConf: safeSearchConf,
IDs: cj.IDs,
Tags: cj.Tags, Name: cj.Name,
IDs: cj.IDs,
Tags: cj.Tags,
BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
UseOwnSettings: !cj.UseGlobalSettings, UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled, FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled, ParentalEnabled: cj.ParentalEnabled,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled, SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
safeSearchConf: safeSearchConf,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices, UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
} }
if cj.IgnoreQueryLog != aghalg.NBNull {
c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue
} else if prev != nil {
c.IgnoreQueryLog = prev.IgnoreQueryLog
}
if cj.IgnoreStatistics != aghalg.NBNull {
c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue
} else if prev != nil {
c.IgnoreStatistics = prev.IgnoreStatistics
}
if safeSearchConf.Enabled {
err = c.setSafeSearch(
safeSearchConf,
clients.safeSearchCacheSize,
clients.safeSearchCacheTTL,
)
if err != nil {
return nil, fmt.Errorf("creating safesearch for client %q: %w", c.Name, err)
}
}
return c, nil
} }
// clientToJSON converts Client object to JSON. // clientToJSON converts Client object to JSON.
@@ -148,6 +181,9 @@ func clientToJSON(c *Client) (cj *clientJSON) {
BlockedServices: c.BlockedServices, BlockedServices: c.BlockedServices,
Upstreams: c.Upstreams, Upstreams: c.Upstreams,
IgnoreQueryLog: aghalg.BoolToNullBool(c.IgnoreQueryLog),
IgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics),
} }
} }
@@ -161,7 +197,13 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
return return
} }
c := jsonToClient(cj) c, err := clients.jsonToClient(cj, nil)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
ok, err := clients.Add(c) ok, err := clients.Add(c)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -209,6 +251,8 @@ type updateJSON struct {
} }
// handleUpdateClient is the handler for POST /control/clients/update HTTP API. // handleUpdateClient is the handler for POST /control/clients/update HTTP API.
//
// TODO(s.chzhen): Accept updated parameters instead of whole structure.
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
dj := updateJSON{} dj := updateJSON{}
err := json.NewDecoder(r.Body).Decode(&dj) err := json.NewDecoder(r.Body).Decode(&dj)
@@ -224,7 +268,27 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
return return
} }
c := jsonToClient(dj.Data) var prev *Client
var ok bool
func() {
clients.lock.Lock()
defer clients.lock.Unlock()
prev, ok = clients.list[dj.Name]
}()
if !ok {
aghhttp.Error(r, w, http.StatusBadRequest, "client not found")
}
c, err := clients.jsonToClient(dj.Data, prev)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
err = clients.Update(dj.Name, c) err = clients.Update(dj.Name, c)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

View File

@@ -296,12 +296,26 @@ var config = &configuration{
MaxGoroutines: 300, MaxGoroutines: 300,
}, },
DnsfilterConf: &filtering.Config{ DnsfilterConf: &filtering.Config{
SafeBrowsingCacheSize: 1 * 1024 * 1024,
SafeSearchCacheSize: 1 * 1024 * 1024,
ParentalCacheSize: 1 * 1024 * 1024,
CacheTime: 30,
FilteringEnabled: true, FilteringEnabled: true,
FiltersUpdateIntervalHours: 24, FiltersUpdateIntervalHours: 24,
ParentalEnabled: false,
SafeBrowsingEnabled: false,
SafeBrowsingCacheSize: 1 * 1024 * 1024,
SafeSearchCacheSize: 1 * 1024 * 1024,
ParentalCacheSize: 1 * 1024 * 1024,
CacheTime: 30,
SafeSearchConf: filtering.SafeSearchConfig{
Enabled: false,
Bing: true,
DuckDuckGo: true,
Google: true,
Pixabay: true,
Yandex: true,
YouTube: true,
},
}, },
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true, UsePrivateRDNS: true,

View File

@@ -13,7 +13,9 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
) )
@@ -97,14 +99,17 @@ func collectDNSAddresses() (addrs []string, err error) {
// statusResponse is a response for /control/status endpoint. // statusResponse is a response for /control/status endpoint.
type statusResponse struct { type statusResponse struct {
Version string `json:"version"` Version string `json:"version"`
Language string `json:"language"` Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"` DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"` DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"` HTTPPort int `json:"http_port"`
IsProtectionEnabled bool `json:"protection_enabled"`
// ProtectionDisabledDuration is a pause duration in milliseconds. // ProtectionDisabledDuration is the duration of the protection pause in
// milliseconds.
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"` ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
ProtectionEnabled bool `json:"protection_enabled"`
// TODO(e.burkov): Inspect if front-end doesn't requires this field as // TODO(e.burkov): Inspect if front-end doesn't requires this field as
// openapi.yaml declares. // openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"` IsDHCPAvailable bool `json:"dhcp_available"`
@@ -121,12 +126,15 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
return return
} }
isProtectionEnabled := false var (
var c *dnsforward.FilteringConfig fltConf *dnsforward.FilteringConfig
protectionDisabledUntil *time.Time
protectionEnabled bool
)
if Context.dnsServer != nil { if Context.dnsServer != nil {
c = &dnsforward.FilteringConfig{} fltConf = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(c) Context.dnsServer.WriteDiskConfig(fltConf)
isProtectionEnabled = Context.dnsServer.UpdatedProtectionStatus() protectionEnabled, protectionDisabledUntil = Context.dnsServer.UpdatedProtectionStatus()
} }
var resp statusResponse var resp statusResponse
@@ -134,20 +142,26 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
config.RLock() config.RLock()
defer config.RUnlock() defer config.RUnlock()
var pauseDuration int64 var protectionDisabledDuration int64
if until := config.DNS.ProtectionDisabledUntil; until != nil { if protectionDisabledUntil != nil {
pauseDuration = time.Until(*until).Milliseconds() // Make sure that we don't send negative numbers to the frontend,
// since enough time might have passed to make the difference less
// than zero.
protectionDisabledDuration = mathutil.Max(
0,
time.Until(*protectionDisabledUntil).Milliseconds(),
)
} }
resp = statusResponse{ resp = statusResponse{
Version: version.Version(), Version: version.Version(),
Language: config.Language,
DNSAddrs: dnsAddrs, DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port, DNSPort: config.DNS.Port,
HTTPPort: config.BindPort, HTTPPort: config.BindPort,
Language: config.Language, ProtectionDisabledDuration: protectionDisabledDuration,
ProtectionEnabled: protectionEnabled,
IsRunning: isRunning(), IsRunning: isRunning(),
ProtectionDisabledDuration: pauseDuration,
IsProtectionEnabled: isProtectionEnabled,
} }
}() }()
@@ -229,7 +243,7 @@ func modifiesData(m string) (ok bool) {
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) { func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
const statusUnsup = http.StatusUnsupportedMediaType const statusUnsup = http.StatusUnsupportedMediaType
cType := r.Header.Get(aghhttp.HdrNameContentType) cType := r.Header.Get(httphdr.ContentType)
if r.ContentLength == 0 { if r.ContentLength == 0 {
if cType == "" { if cType == "" {
return true return true
@@ -318,13 +332,17 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
return false return false
} }
var serveHTTP3 bool var (
var portHTTPS int forceHTTPS bool
serveHTTP3 bool
portHTTPS int
)
func() { func() {
config.RLock() config.RLock()
defer config.RUnlock() defer config.RUnlock()
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
forceHTTPS = config.TLS.ForceHTTPS && config.TLS.Enabled && config.TLS.PortHTTPS != 0
}() }()
respHdr := w.Header() respHdr := w.Header()
@@ -337,13 +355,13 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
// default is 24 hours. // default is 24 hours.
if serveHTTP3 { if serveHTTP3 {
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS) altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc) respHdr.Set(httphdr.AltSvc, altSvc)
} }
if r.TLS == nil && web.forceHTTPS { if r.TLS == nil && forceHTTPS {
hostPort := host hostPort := host
if port := web.conf.PortHTTPS; port != defaultPortHTTPS { if portHTTPS != defaultPortHTTPS {
hostPort = netutil.JoinHostPort(host, port) hostPort = netutil.JoinHostPort(host, portHTTPS)
} }
httpsURL := &url.URL{ httpsURL := &url.URL{
@@ -367,8 +385,8 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
Host: r.Host, Host: r.Host,
} }
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String()) respHdr.Set(httphdr.AccessControlAllowOrigin, originURL.String())
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin) respHdr.Set(httphdr.Vary, httphdr.Origin)
return true return true
} }
@@ -381,7 +399,7 @@ func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.Res
path := r.URL.Path path := r.URL.Path
if Context.firstRun && !strings.HasPrefix(path, "/install.") && if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
!strings.HasPrefix(path, "/assets/") { !strings.HasPrefix(path, "/assets/") {
http.Redirect(w, r, "/install.html", http.StatusFound) http.Redirect(w, r, "install.html", http.StatusFound)
return return
} }

View File

@@ -39,7 +39,7 @@ type getAddrsResponse struct {
} }
// handleInstallGetAddresses is the handler for /install/get_addresses endpoint. // handleInstallGetAddresses is the handler for /install/get_addresses endpoint.
func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { func (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := getAddrsResponse{ data := getAddrsResponse{
Version: version.Version(), Version: version.Version(),
@@ -167,7 +167,7 @@ func (req *checkConfReq) validateDNS(
} }
// handleInstallCheckConfig handles the /check_config endpoint. // handleInstallCheckConfig handles the /check_config endpoint.
func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) { func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
req := &checkConfReq{} req := &checkConfReq{}
err := json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
@@ -375,7 +375,7 @@ func shutdownSrv3(srv *http3.Server) {
const PasswordMinRunes = 8 const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server // Apply new configuration, start DNS server, restart Web server
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
req, restartHTTP, err := decodeApplyConfigReq(r.Body) req, restartHTTP, err := decodeApplyConfigReq(r.Body)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -503,7 +503,7 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
return req, restartHTTP, err return req, restartHTTP, err
} }
func (web *Web) registerInstallHandlers() { func (web *webAPI) registerInstallHandlers() {
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses))) Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig))) Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure))) Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))

View File

@@ -51,11 +51,12 @@ func initDNS() (err error) {
anonymizer := config.anonymizer() anonymizer := config.anonymizer()
statsConf := stats.Config{ statsConf := stats.Config{
Filename: filepath.Join(baseDir, "stats.db"), Filename: filepath.Join(baseDir, "stats.db"),
Limit: config.Stats.Interval.Duration, Limit: config.Stats.Interval.Duration,
ConfigModified: onConfigModified, ConfigModified: onConfigModified,
HTTPRegister: httpRegister, HTTPRegister: httpRegister,
Enabled: config.Stats.Enabled, Enabled: config.Stats.Enabled,
ShouldCountClient: Context.clients.shouldCountClient,
} }
set, err := aghnet.NewDomainNameSet(config.Stats.Ignored) set, err := aghnet.NewDomainNameSet(config.Stats.Ignored)
@@ -545,6 +546,8 @@ var _ filtering.Resolver = safeSearchResolver{}
// LookupIP implements [filtering.Resolver] interface for safeSearchResolver. // LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
// It returns the slice of net.IP with IPv4 and IPv6 instances. // It returns the slice of net.IP with IPv4 and IPv6 instances.
//
// TODO(a.garipov): Support network.
func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) { func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
addrs, err := Context.dnsServer.Resolve(host) addrs, err := Context.dnsServer.Resolve(host)
if err != nil { if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
@@ -59,7 +58,7 @@ type homeContext struct {
dhcpServer dhcpd.Interface // DHCP module dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering module filters *filtering.DNSFilter // DNS filtering module
web *Web // Web (HTTP, HTTPS) module web *webAPI // Web (HTTP, HTTPS) module
tls *tlsManager // TLS module tls *tlsManager // TLS module
// etcHosts contains IP-hostname mappings taken from the OS-specific hosts // etcHosts contains IP-hostname mappings taken from the OS-specific hosts
@@ -297,8 +296,9 @@ func setupConfig(opts options) (err error) {
config.DNS.DnsfilterConf.HTTPClient = Context.client config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{} config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefaultSafeSearch( config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefault(
config.DNS.DnsfilterConf.SafeSearchConf, config.DNS.DnsfilterConf.SafeSearchConf,
"default",
config.DNS.DnsfilterConf.SafeSearchCacheSize, config.DNS.DnsfilterConf.SafeSearchCacheSize,
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime), time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
) )
@@ -387,7 +387,7 @@ func checkPorts() (err error) {
return nil return nil
} }
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) { func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) {
var clientFS fs.FS var clientFS fs.FS
if opts.localFrontend { if opts.localFrontend {
log.Info("warning: using local frontend files") log.Info("warning: using local frontend files")
@@ -414,7 +414,7 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
serveHTTP3: config.DNS.ServeHTTP3, serveHTTP3: config.DNS.ServeHTTP3,
} }
web = newWeb(&webConf) web = newWebAPI(&webConf)
if web == nil { if web == nil {
return nil, fmt.Errorf("initializing web: %w", err) return nil, fmt.Errorf("initializing web: %w", err)
} }
@@ -469,26 +469,8 @@ func run(opts options, clientBuildFS fs.FS) {
fatalOnError(err) fatalOnError(err)
if config.DebugPProf { if config.DebugPProf {
mux := http.NewServeMux() // TODO(a.garipov): Make the address configurable.
mux.HandleFunc("/debug/pprof/", pprof.Index) startPprof("localhost:6060")
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
go func() {
log.Info("pprof: listening on localhost:6060")
lerr := http.ListenAndServe("localhost:6060", mux)
log.Error("Error while running the pprof server: %s", lerr)
}()
} }
} }
@@ -551,7 +533,7 @@ func run(opts options, clientBuildFS fs.FS) {
} }
} }
Context.web.Start() Context.web.start()
// wait indefinitely for other go-routines to complete their job // wait indefinitely for other go-routines to complete their job
select {} select {}
@@ -731,7 +713,7 @@ func cleanup(ctx context.Context) {
log.Info("stopping AdGuard Home") log.Info("stopping AdGuard Home")
if Context.web != nil { if Context.web != nil {
Context.web.Close(ctx) Context.web.close(ctx)
Context.web = nil Context.web = nil
} }
if Context.auth != nil { if Context.auth != nil {
@@ -869,8 +851,10 @@ func detectFirstRun() bool {
// Connect to a remote server resolving hostname using our own DNS server. // Connect to a remote server resolving hostname using our own DNS server.
// //
// TODO(e.burkov): This messy logic should be decomposed and clarified. // TODO(e.burkov): This messy logic should be decomposed and clarified.
//
// TODO(a.garipov): Support network.
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) { func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
log.Tracef("network:%v addr:%v", network, addr) log.Debug("home: customdial: dialing addr %q for network %s", addr, network)
host, port, err := net.SplitHostPort(addr) host, port, err := net.SplitHostPort(addr)
if err != nil { if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/google/uuid" "github.com/google/uuid"
"howett.net/plist" "howett.net/plist"
@@ -170,7 +171,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
return return
} }
w.Header().Set("Content-Type", "application/xml") w.Header().Set(httphdr.ContentType, "application/xml")
const ( const (
dohContDisp = `attachment; filename=doh.mobileconfig` dohContDisp = `attachment; filename=doh.mobileconfig`
@@ -182,7 +183,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
contDisp = dotContDisp contDisp = dotContDisp
} }
w.Header().Set("Content-Disposition", contDisp) w.Header().Set(httphdr.ContentDisposition, contDisp)
_, _ = w.Write(mobileconfig) _, _ = w.Write(mobileconfig)
} }

View File

@@ -249,13 +249,15 @@ var cmdLineOpts = []cmdLineOpt{{
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil }, updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
effect: func(_ options, _ string) (f effect, err error) { effect: func(_ options, _ string) (f effect, err error) {
log.Info( log.Info(
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions", "warning: --no-etc-hosts flag is deprecated " +
"and will be removed in the future versions; " +
"set clients.runtime_sources.hosts in the configuration file to false instead",
) )
return nil, nil return nil, nil
}, },
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts }, serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
description: "Deprecated. Do not use the OS-provided hosts.", description: "Deprecated: use clients.runtime_sources.hosts instead. Do not use the OS-provided hosts.",
longName: "no-etc-hosts", longName: "no-etc-hosts",
shortName: "", shortName: "",
}, { }, {

39
internal/home/pprof.go Normal file
View File

@@ -0,0 +1,39 @@
package home
import (
"net/http"
"net/http/pprof"
"runtime"
"github.com/AdguardTeam/golibs/log"
)
// startPprof launches the debug and profiling server on addr.
func startPprof(addr string) {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
go func() {
defer log.OnPanic("pprof server")
log.Info("pprof: listening on %q", addr)
err := http.ListenAndServe(addr, mux)
log.Info("pprof server errors: %v", err)
}()
}

View File

@@ -108,7 +108,7 @@ func (m *tlsManager) start() {
// The background context is used because the TLSConfigChanged wraps context // The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current // with timeout on its own and shuts down the server, which handles current
// request. // request.
Context.web.TLSConfigChanged(context.Background(), tlsConf) Context.web.tlsConfigChanged(context.Background(), tlsConf)
} }
// reload updates the configuration and restarts t. // reload updates the configuration and restarts t.
@@ -156,7 +156,7 @@ func (m *tlsManager) reload() {
// The background context is used because the TLSConfigChanged wraps context // The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current // with timeout on its own and shuts down the server, which handles current
// request. // request.
Context.web.TLSConfigChanged(context.Background(), tlsConf) Context.web.tlsConfigChanged(context.Background(), tlsConf)
} }
// loadTLSConf loads and validates the TLS configuration. The returned error is // loadTLSConf loads and validates the TLS configuration. The returned error is
@@ -454,7 +454,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
// same reason. // same reason.
if restartHTTPS { if restartHTTPS {
go func() { go func() {
Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings) Context.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
}() }()
} }
} }

View File

@@ -35,9 +35,8 @@ const (
type webConfig struct { type webConfig struct {
clientFS fs.FS clientFS fs.FS
BindHost netip.Addr BindHost netip.Addr
BindPort int BindPort int
PortHTTPS int
// ReadTimeout is an option to pass to http.Server for setting an // ReadTimeout is an option to pass to http.Server for setting an
// appropriate field. // appropriate field.
@@ -72,8 +71,8 @@ type httpsServer struct {
enabled bool enabled bool
} }
// Web is the web UI and API server. // webAPI is the web UI and API server.
type Web struct { type webAPI struct {
conf *webConfig conf *webConfig
// TODO(a.garipov): Refactor all these servers. // TODO(a.garipov): Refactor all these servers.
@@ -82,15 +81,13 @@ type Web struct {
// httpsServer is the server that handles HTTPS traffic. If it is not nil, // httpsServer is the server that handles HTTPS traffic. If it is not nil,
// [Web.http3Server] must also not be nil. // [Web.http3Server] must also not be nil.
httpsServer httpsServer httpsServer httpsServer
forceHTTPS bool
} }
// newWeb creates a new instance of the web UI and API server. // newWebAPI creates a new instance of the web UI and API server.
func newWeb(conf *webConfig) (w *Web) { func newWebAPI(conf *webConfig) (w *webAPI) {
log.Info("web: initializing") log.Info("web: initializing")
w = &Web{ w = &webAPI{
conf: conf, conf: conf,
} }
@@ -125,12 +122,10 @@ func webCheckPortAvailable(port int) (ok bool) {
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
} }
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server // tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary. // if necessary.
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) { func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
log.Debug("web: applying new tls configuration") log.Debug("web: applying new tls configuration")
web.conf.PortHTTPS = tlsConf.PortHTTPS
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
enabled := tlsConf.Enabled && enabled := tlsConf.Enabled &&
tlsConf.PortHTTPS != 0 && tlsConf.PortHTTPS != 0 &&
@@ -161,8 +156,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
} }
// Start - start serving HTTP requests // start - start serving HTTP requests
func (web *Web) Start() { func (web *webAPI) start() {
log.Println("AdGuard Home is available at the following addresses:") log.Println("AdGuard Home is available at the following addresses:")
// for https, we have a separate goroutine loop // for https, we have a separate goroutine loop
@@ -203,8 +198,8 @@ func (web *Web) Start() {
} }
} }
// Close gracefully shuts down the HTTP servers. // close gracefully shuts down the HTTP servers.
func (web *Web) Close(ctx context.Context) { func (web *webAPI) close(ctx context.Context) {
log.Info("stopping http server...") log.Info("stopping http server...")
web.httpsServer.cond.L.Lock() web.httpsServer.cond.L.Lock()
@@ -222,7 +217,7 @@ func (web *Web) Close(ctx context.Context) {
log.Info("stopped http server") log.Info("stopped http server")
} }
func (web *Web) tlsServerLoop() { func (web *webAPI) tlsServerLoop() {
for { for {
web.httpsServer.cond.L.Lock() web.httpsServer.cond.L.Lock()
if web.httpsServer.inShutdown { if web.httpsServer.inShutdown {
@@ -241,7 +236,15 @@ func (web *Web) tlsServerLoop() {
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS) var portHTTPS int
func() {
config.RLock()
defer config.RUnlock()
portHTTPS = config.TLS.PortHTTPS
}()
addr := netutil.JoinHostPort(web.conf.BindHost.String(), portHTTPS)
web.httpsServer.server = &http.Server{ web.httpsServer.server = &http.Server{
ErrorLog: log.StdLog("web: https", log.DEBUG), ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: addr, Addr: addr,
@@ -272,7 +275,7 @@ func (web *Web) tlsServerLoop() {
} }
} }
func (web *Web) mustStartHTTP3(address string) { func (web *webAPI) mustStartHTTP3(address string) {
defer log.OnPanic("web: http3") defer log.OnPanic("web: http3")
web.httpsServer.server3 = &http3.Server{ web.httpsServer.server3 = &http3.Server{

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@@ -99,8 +100,8 @@ func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) { func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
// TODO(a.garipov): Put some of these to a middleware. // TODO(a.garipov): Put some of these to a middleware.
h := w.Header() h := w.Header()
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON) h.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent()) h.Set(httphdr.Server, aghhttp.UserAgent())
w.WriteHeader(code) w.WriteHeader(code)

View File

@@ -1,13 +1,18 @@
package websvc package websvc
import "net/http" import (
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/httphdr"
)
// Middlewares // Middlewares
// jsonMw sets the content type of the response to application/json. // jsonMw sets the content type of the response to application/json.
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) { func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
f := func(w http.ResponseWriter, r *http.Request) { f := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
} }

View File

@@ -7,6 +7,7 @@ type Client struct {
Name string `json:"name"` Name string `json:"name"`
DisallowedRule string `json:"disallowed_rule"` DisallowedRule string `json:"disallowed_rule"`
Disallowed bool `json:"disallowed"` Disallowed bool `json:"disallowed"`
IgnoreQueryLog bool `json:"-"`
} }
// ClientWHOIS is the filtered WHOIS data for the client. // ClientWHOIS is the filtered WHOIS data for the client.

View File

@@ -58,11 +58,11 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
var err error var err error
if isOrig { if isOrig {
e.Answer, err = resp.Pack()
err = errors.Annotate(err, "packing answer: %w")
} else {
e.OrigAnswer, err = resp.Pack() e.OrigAnswer, err = resp.Pack()
err = errors.Annotate(err, "packing orig answer: %w") err = errors.Annotate(err, "packing orig answer: %w")
} else {
e.Answer, err = resp.Pack()
err = errors.Annotate(err, "packing answer: %w")
} }
if err != nil { if err != nil {
log.Error("querylog: %s", err) log.Error("querylog: %s", err)

View File

@@ -247,10 +247,19 @@ func (l *queryLog) Add(params *AddParams) {
} }
// ShouldLog returns true if request for the host should be logged. // ShouldLog returns true if request for the host should be logged.
func (l *queryLog) ShouldLog(host string, _, _ uint16) bool { func (l *queryLog) ShouldLog(host string, _, _ uint16, ids []string) bool {
l.confMu.RLock() l.confMu.RLock()
defer l.confMu.RUnlock() defer l.confMu.RUnlock()
c, err := l.findClient(ids)
if err != nil {
log.Error("querylog: finding client: %s", err)
}
if c != nil && c.IgnoreQueryLog {
return false
}
return !l.isIgnored(host) return !l.isIgnored(host)
} }

View File

@@ -258,36 +258,52 @@ func TestQueryLogShouldLog(t *testing.T) {
) )
set := stringutil.NewSet(ignored1, ignored2) set := stringutil.NewSet(ignored1, ignored2)
findClient := func(ids []string) (c *Client, err error) {
log := ids[0] == "no_log"
return &Client{IgnoreQueryLog: log}, nil
}
l, err := newQueryLog(Config{ l, err := newQueryLog(Config{
Ignored: set, Ignored: set,
Enabled: true, Enabled: true,
RotationIvl: timeutil.Day, RotationIvl: timeutil.Day,
MemSize: 100, MemSize: 100,
BaseDir: t.TempDir(), BaseDir: t.TempDir(),
FindClient: findClient,
}) })
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
name string name string
host string host string
ids []string
wantLog bool wantLog bool
}{{ }{{
name: "log", name: "log",
host: "example.com", host: "example.com",
ids: []string{"whatever"},
wantLog: true, wantLog: true,
}, { }, {
name: "no_log_ignored_1", name: "no_log_ignored_1",
host: ignored1, host: ignored1,
ids: []string{"whatever"},
wantLog: false, wantLog: false,
}, { }, {
name: "no_log_ignored_2", name: "no_log_ignored_2",
host: ignored2, host: ignored2,
ids: []string{"whatever"},
wantLog: false,
}, {
name: "no_log_client_ignore",
host: "example.com",
ids: []string{"no_log"},
wantLog: false, wantLog: false,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
res := l.ShouldLog(tc.host, dns.TypeA, dns.ClassINET) res := l.ShouldLog(tc.host, dns.TypeA, dns.ClassINET, tc.ids)
assert.Equal(t, tc.wantLog, res) assert.Equal(t, tc.wantLog, res)
}) })

View File

@@ -29,7 +29,7 @@ type QueryLog interface {
WriteDiskConfig(c *Config) WriteDiskConfig(c *Config)
// ShouldLog returns true if request for the host should be logged. // ShouldLog returns true if request for the host should be logged.
ShouldLog(host string, qType, qClass uint16) bool ShouldLog(host string, qType, qClass uint16, ids []string) bool
} }
// Config is the query log configuration structure. // Config is the query log configuration structure.

View File

@@ -288,6 +288,10 @@ func (l *queryLog) readNextEntry(
// Go on and try to match anyway. // Go on and try to match anyway.
} }
if e.client != nil && e.client.IgnoreQueryLog {
return nil, ts, nil
}
ts = e.Time.UnixNano() ts = e.Time.UnixNano()
if !params.match(e) { if !params.match(e) {
return nil, ts, nil return nil, ts, nil

View File

@@ -24,18 +24,19 @@ func TestHandleStatsConfig(t *testing.T) {
) )
conf := Config{ conf := Config{
UnitID: func() (id uint32) { return 0 }, UnitID: func() (id uint32) { return 0 },
ConfigModified: func() {}, ConfigModified: func() {},
Filename: filepath.Join(t.TempDir(), "stats.db"), ShouldCountClient: func([]string) bool { return true },
Limit: time.Hour * 24, Filename: filepath.Join(t.TempDir(), "stats.db"),
Enabled: true, Limit: time.Hour * 24,
Enabled: true,
} }
testCases := []struct { testCases := []struct {
name string name string
wantErr string
body getConfigResp body getConfigResp
wantCode int wantCode int
wantErr string
}{{ }{{
name: "set_ivl_1_minIvl", name: "set_ivl_1_minIvl",
body: getConfigResp{ body: getConfigResp{

View File

@@ -52,6 +52,9 @@ type Config struct {
// interface. // interface.
ConfigModified func() ConfigModified func()
// ShouldCountClient returns client's ignore setting.
ShouldCountClient func([]string) bool
// HTTPRegister is the function that registers handlers for the stats // HTTPRegister is the function that registers handlers for the stats
// endpoints. // endpoints.
HTTPRegister aghhttp.RegisterFunc HTTPRegister aghhttp.RegisterFunc
@@ -87,7 +90,7 @@ type Interface interface {
WriteDiskConfig(dc *Config) WriteDiskConfig(dc *Config)
// ShouldCount returns true if request for the host should be counted. // ShouldCount returns true if request for the host should be counted.
ShouldCount(host string, qType, qClass uint16) bool ShouldCount(host string, qType, qClass uint16, ids []string) bool
} }
// StatsCtx collects the statistics and flushes it to the database. Its default // StatsCtx collects the statistics and flushes it to the database. Its default
@@ -118,6 +121,9 @@ type StatsCtx struct {
// ignored is the list of host names, which should not be counted. // ignored is the list of host names, which should not be counted.
ignored *stringutil.Set ignored *stringutil.Set
// shouldCountClient returns client's ignore setting.
shouldCountClient func([]string) bool
// filename is the name of database file. // filename is the name of database file.
filename string filename string
@@ -138,16 +144,21 @@ func New(conf Config) (s *StatsCtx, err error) {
return nil, fmt.Errorf("unsupported interval: %w", err) return nil, fmt.Errorf("unsupported interval: %w", err)
} }
if conf.ShouldCountClient == nil {
return nil, errors.Error("should count client is unspecified")
}
s = &StatsCtx{ s = &StatsCtx{
currMu: &sync.RWMutex{}, currMu: &sync.RWMutex{},
httpRegister: conf.HTTPRegister, httpRegister: conf.HTTPRegister,
configModified: conf.ConfigModified, configModified: conf.ConfigModified,
filename: conf.Filename, filename: conf.Filename,
confMu: &sync.RWMutex{}, confMu: &sync.RWMutex{},
ignored: conf.Ignored, ignored: conf.Ignored,
limit: conf.Limit, shouldCountClient: conf.ShouldCountClient,
enabled: conf.Enabled, limit: conf.Limit,
enabled: conf.Enabled,
} }
if s.unitIDGen = newUnitID; conf.UnitID != nil { if s.unitIDGen = newUnitID; conf.UnitID != nil {
@@ -577,10 +588,14 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, firstID uint32) {
} }
// ShouldCount returns true if request for the host should be counted. // ShouldCount returns true if request for the host should be counted.
func (s *StatsCtx) ShouldCount(host string, _, _ uint16) bool { func (s *StatsCtx) ShouldCount(host string, _, _ uint16, ids []string) bool {
s.confMu.RLock() s.confMu.RLock()
defer s.confMu.RUnlock() defer s.confMu.RUnlock()
if !s.shouldCountClient(ids) {
return false
}
return !s.isIgnored(host) return !s.isIgnored(host)
} }

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