Compare commits

...

30 Commits

Author SHA1 Message Date
Dimitry Kolyshev
94458c5658 Merge remote-tracking branch 'origin/master' into 3389-querylog-export
# Conflicts:
#	CHANGELOG.md
2023-06-22 13:28:17 +04:00
Ainar Garipov
123ca87388 Pull request 1885: AG-23334-fix-snap-plan
Merge in DNS/adguard-home from AG-23334-fix-snap-plan to master

Squashed commit of the following:

commit 5d632d1d63c56911e005d0e772e82a509302e948
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 21 17:42:36 2023 +0300

    bamboo-specs: fix snap build; fmt
2023-06-21 17:47:16 +03:00
Ainar Garipov
994906fbd4 Pull request 1884: AG-23334-split-snap
Merge in DNS/adguard-home from AG-23334-split-snap to master

Squashed commit of the following:

commit 5a3d0f105d6930a0868f342821618a2a4acae282
Merge: bba693db6 06d465b0d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 21 17:10:15 2023 +0300

    Merge branch 'master' into AG-23334-split-snap

commit bba693db60fc7e154df3bc6bf186292ee9bc4ed5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 21 16:50:45 2023 +0300

    scripts/snap: fix docs; imp data

commit cb4a1d5bed147a41dda69b80b7ae6c3902c26538
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 21 14:03:48 2023 +0300

    all: fix scripts; imp docs

commit f88320b16ed7679e151a5358f4ac8e0212b4a827
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 18:52:21 2023 +0300

    all: split snap ci
2023-06-21 17:14:10 +03:00
Stanislav Chzhen
06d465b0d1 Pull request 1858: AG-22594-imp-whois
Merge in DNS/adguard-home from AG-22594-imp-whois to master

Squashed commit of the following:

commit 093feed53291d02469fb1bd8d99472597ebd5015
Merge: 956d20dc4 ca313521d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 21 12:42:40 2023 +0300

    Merge branch 'master' into AG-22594-imp-whois

commit 956d20dc473dcec90895b6f618fc56e96e9ff833
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 20 18:30:48 2023 +0300

    whois: imp code more

commit c771fd9c5e4d90e76d079a0d25ab097ab5652a42
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 20 15:05:45 2023 +0300

    whois: imp code

commit 21900fd468e10d9aee22149a6312b8596ff39810
Merge: 8dbe132c0 371261b2c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 20 11:34:06 2023 +0300

    Merge branch 'master' into AG-22594-imp-whois

commit 8dbe132c08d3ad4a63b0d4bdb9d00a5bc25971f4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 20 11:33:26 2023 +0300

    whois: imp code more

commit f5e761a260237579c67cbd48f01ea90499bff6b0
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jun 19 16:04:35 2023 +0300

    whois: imp code

commit 2780f7e16aacddad8736f83b77ef9bfa1271f8b1
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 16 17:33:47 2023 +0300

    all: imp code

commit 1fc67016068b745a46b3d0d341ab14f9f5bdc9aa
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 16 17:29:19 2023 +0300

    whois: imp tests

commit 204761870764fb10feea20065d79dee8c321e70b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 16 11:55:37 2023 +0300

    all: upd deps

commit ded4f59498c5c544277b9c8e249567626547680e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 14 20:43:32 2023 +0300

    all: imp tests

commit 0eed9834ff9dd94d0788ce69d0bb0521fa725410
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 14 19:31:49 2023 +0300

    all: imp code

commit 9f867587c8ad87363b8c8b061ead536c1ec59c5d
Merge: 504e9484d 681c604c2
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 13 14:20:44 2023 +0300

    Merge branch 'master' into AG-22594-imp-whois

commit 504e9484dd84ab9d7c84a3f8399993d6422d3b67
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 13 14:18:06 2023 +0300

    all: imp cache

commit c492abe41ace7ad76fcd4e297c22b910a90fec30
Merge: db36adb9c 826b314f1
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 9 16:06:12 2023 +0300

    Merge branch 'master' into AG-22594-imp-whois

commit db36adb9c14ce92b3971db0d87ec313d5bcd787e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 9 15:53:33 2023 +0300

    all: add todo

commit 5cf192de9f93cd0d8521a3a6b4ded7f2bc5e0031
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 8 14:59:26 2023 +0300

    all: imp docs

commit 021aa3eb5b9476a93b4af5fc90cc9ccf014ca152
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jun 5 18:35:25 2023 +0300

    all: imp naming

commit 4626c3a7fa3f2543501806c9fa1a19531547f394
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 2 17:41:00 2023 +0300

    all: imp tests

commit 1afcc9605ca176e4c7f76a03a2c996cf7d6bde13
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 2 12:44:32 2023 +0300

    all: imp docs

commit cdd0544ff1a63faed5ced3dae6bfb3b783e45428
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 1 17:21:37 2023 +0300

    all: add docs

... and 2 more commits
2023-06-21 12:53:53 +03:00
Ainar Garipov
ca313521dc Pull request 1881: 5913-fix-safesearch-ipv6-better
Updates #5913.

Squashed commit of the following:

commit 6bff5ee1b77ae1812e2803361e60ef12148da5c7
Merge: 0a6f49008 2902f030b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 14:14:47 2023 +0300

    Merge branch 'master' into 5913-fix-safesearch-ipv6-better

commit 0a6f49008ac1c786ba380cc3c9a4c2c0c1f60815
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 14:11:18 2023 +0300

    safesearch: imp tests

commit 3f9056d26816fb753a394a7bcf86f2ae1201d19c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 19 23:01:03 2023 +0300

    safesearch: fix ipv6 more
2023-06-20 14:48:36 +03:00
Ainar Garipov
2902f030be Pull request 1879: nextapi-opts
Merge in DNS/adguard-home from nextapi-opts to master

Squashed commit of the following:

commit 01f27e374785f47f41470126ddc17cf447e84dd7
Merge: 17d3b06e0 371261b2c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 13:43:08 2023 +0300

    Merge branch 'master' into nextapi-opts

commit 17d3b06e0551908b06be67233181ce4067b6c1cc
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 13:15:31 2023 +0300

    next: imp chlog

commit 19d5ea9db0ee077c55c9f1d54a07a3c6b56b4e2e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 20 12:58:48 2023 +0300

    cmd: typo

commit 082ad5b5fc634a1533127e428680ccf55c0811ab
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 19 22:49:16 2023 +0300

    cmd: imp api, docs, names

commit a49b3cbcc591f36530306c2e08cf1ea7dc8f6d30
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 19 17:13:50 2023 +0300

    next/cmd: add opt parsing
2023-06-20 14:11:34 +03:00
Dimitry Kolyshev
371261b2c6 Pull request: dnsforward: stat empty queries
Merge in DNS/adguard-home from 5910-stats-malformed-entry to master

Squashed commit of the following:

commit ea6718daf0d65dc4b13cd08430da7fdeb6f7fd5b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 17:34:50 2023 +0400

    all: imp docs

commit 5341c5b351c87556b91a7f692960edc76f44b21a
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 16:47:13 2023 +0400

    all: docs

commit 1703c421f501484a80ddcf4ca1ba24f71fa5d225
Merge: f56b62490 d26c480d0
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 16:46:13 2023 +0400

    Merge remote-tracking branch 'origin/master' into 5910-stats-malformed-entry

    # Conflicts:
    #	CHANGELOG.md

commit f56b62490c7033e1d0b620a9cb4ba8de64862de0
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 14:49:43 2023 +0400

    all: docs

commit be954a8cab950d038c8cf08b95158069fa0747ba
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 14:44:05 2023 +0400

    querylog: empty queries

commit 0cda309d402156e750c7387e471da6795aa43bd6
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 19 14:26:41 2023 +0400

    dnsforward: stat empty queries
2023-06-20 10:36:20 +03:00
Ainar Garipov
d26c480d03 Pull request 1877: 5913-fix-safesearch-ipv6
Updates #5913.

Squashed commit of the following:

commit a0ab1320ea22dc1b4e2804ef2d14e0091daa6a1e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 19 15:23:44 2023 +0300

    all: fmt; typo

commit 3a2e561c535bbbd2b2eeeaa1a6f423bc123b1a6b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jun 19 15:16:28 2023 +0300

    all: fix safesearch for ipv6
2023-06-19 15:45:01 +03:00
Ainar Garipov
b6d00f774b Pull request 1875: next-gocognit
Merge in DNS/adguard-home from next-gocognit to master

Squashed commit of the following:

commit 554458a63b44e71244b4364168e6e9903b880638
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 16 19:58:14 2023 +0300

    all: add tool; typo

commit 1b97f93811d1ea826762cfa43fef49ae43e06939
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 16 19:49:40 2023 +0300

    all: add gocognit
2023-06-19 12:21:32 +03:00
Dimitry Kolyshev
6fea435d89 Merge remote-tracking branch 'origin/master' into 3389-querylog-export
# Conflicts:
#	CHANGELOG.md
2023-06-15 14:40:06 +04:00
Dimitry Kolyshev
05706bd7ea querylog: bench search 2023-06-15 14:38:44 +04:00
Stanislav Chzhen
d3ada9881a Pull request 1854: 951-blocked-services-schedule
Merge in DNS/adguard-home from 951-blocked-services-schedule to master

Squashed commit of the following:

commit 0f0770292f8aa9dc0dddc05edefacf6655f329b8
Merge: 2705fbcfb 7309a5335
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 14 15:00:56 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule

commit 2705fbcfba57392bcc0fa9b6e1f1dfdce9796963
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 14 14:56:01 2023 +0300

    schedule: imp field alignment

commit ff8fa040558e97192a5a2c91c1d5722c67d88b3d
Merge: f99936f36 9fda7bfd3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jun 13 11:49:15 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule

commit f99936f36be3453b50d772ce10bfdc1ad14879c5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 9 19:14:06 2023 +0300

    schedule: add tests

commit 8436c2c5675d5cc22a3554ded2ce4a49eeeefc91
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 8 20:10:03 2023 +0300

    schedule: imp code

commit 850a3d93e659e0584bd449cf5af3f13b36be9e62
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 8 18:57:23 2023 +0300

    schedule: imp tests

commit d54c42b782a4a806d2f35abbea1826ba6394a64c
Merge: 21dfa51ea 156c199bb
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 8 18:12:03 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule

commit 21dfa51ea7441c655ac39f2cbceeaa598d364bff
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 8 18:02:47 2023 +0300

    all: add todo

commit d0d4532c8911fb6365f6af66ceb72a70b158b77d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 7 18:54:44 2023 +0300

    all: upd chlog

commit dc1d55fa23c0ec8e7f7b7cd25d3d0cbf3e51e236
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 7 17:34:28 2023 +0300

    all: add tests

commit 601775307c2b23fa9d6cf2aa271793e8fd5f1797
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jun 7 13:20:00 2023 +0300

    all: add schedule pkg

commit e0bcad0193a559a89f5f5052f5a985fa730d23be
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 2 13:03:04 2023 +0300

    filtering: add test case

commit da7a7c20c84151ff83414cb5d576a91740a1be7e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 31 15:31:48 2023 +0300

    filtering: imp code

commit 9845a11cdae25eafe39a50f8cdc408fefe1fd746
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 29 17:46:24 2023 +0300

    filtering: add type check

commit f4bcea8ad90d584ceff236ccce4f246e391e0e33
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 29 17:37:25 2023 +0300

    all: imp code

commit 9de3a0f37eb65d0db2431c27dd4d69136c1485a7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 29 15:20:33 2023 +0300

    filtering: imp err msg

commit 1befab59cf2a12a3bc8296066ce1115dff5a8843
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 29 14:31:14 2023 +0300

    filtering: imp tests

commit 09506f924f363f1563009cec0c44ad40d08b8ddd
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri May 26 18:44:12 2023 +0300

    filtering: add tests

commit 36c42f523fcfe7fa6dca5f6f13e6c79f491c1ad5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri May 26 17:57:01 2023 +0300

    all: fix typos

commit 69399cd3ea17691ff0848baf43ff09b18b82114c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu May 25 18:50:51 2023 +0300

    all: upd chlog

... and 2 more commits
2023-06-14 15:08:57 +03:00
Ainar Garipov
7309a53356 Pull request 1874: upd-github-templates
Merge in DNS/adguard-home from upd-github-templates to master

Squashed commit of the following:

commit 713a98668e5ee1fa3d64881496043b499ad6ac09
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 13 20:10:35 2023 +0300

    all: imp github issue templates more

commit 308a3fcd5564d7536b6ae1192cd17c04b6752ea1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 13 19:38:28 2023 +0300

    all: imp issue tmpls
2023-06-14 13:09:09 +03:00
Dimitry Kolyshev
00327757e1 querylog: bench search 2023-06-14 12:07:16 +04:00
Dimitry Kolyshev
5f0e53ded7 home: write timeout middleware 2023-06-14 10:51:17 +04:00
Dimitry Kolyshev
5cd4ce766d Merge remote-tracking branch 'origin/master' into 3389-querylog-export 2023-06-14 10:08:11 +04:00
Dimitry Kolyshev
e695fd9885 querylog: timeout revert 2023-06-14 10:07:22 +04:00
Dimitry Kolyshev
c43053e7d2 querylog: timeout 2023-06-13 14:36:31 +04:00
Dimitry Kolyshev
86e25944b3 querylog: imp code 2023-06-13 13:08:15 +04:00
Dimitry Kolyshev
fd7260f6de Merge remote-tracking branch 'origin/master' into 3389-querylog-export
# Conflicts:
#	CHANGELOG.md
2023-06-13 10:38:17 +04:00
Dimitry Kolyshev
c591e46254 querylog: imp code 2023-06-02 15:26:04 +03:00
Dimitry Kolyshev
66d9ea7cca querylog: imp docs 2023-06-02 15:15:37 +03:00
Dimitry Kolyshev
dafc785845 querylog: imp code docs 2023-06-02 12:09:50 +03:00
Dimitry Kolyshev
e9b17891bb Merge remote-tracking branch 'origin/master' into 3389-querylog-export 2023-06-02 12:00:57 +03:00
Dimitry Kolyshev
0b27f048a7 querylog: imp code 2023-05-31 16:26:56 +03:00
Dimitry Kolyshev
649454e77b querylog: imp code 2023-05-31 15:11:33 +03:00
Dimitry Kolyshev
ca22d8524d all: imp docs 2023-05-31 15:11:15 +03:00
Dimitry Kolyshev
07f4f0474c all: imp docs 2023-05-31 12:06:13 +03:00
Dimitry Kolyshev
8813e135b6 querylog: export 2023-05-31 11:49:15 +03:00
Dimitry Kolyshev
f4f2c11eb9 all: export querylog docs 2023-05-29 13:29:41 +03:00
72 changed files with 3602 additions and 1157 deletions

View File

@@ -10,52 +10,58 @@
- 'label': >
I have checked the
[Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions)
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
and found no answer
'required': true
- 'label': >
I have searched other issues and found no duplicates
'required': true
- 'label': >
I want to report a bug and not ask a question
I want to report a bug and not [ask a question or ask for
help](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
'required': true
- 'label': >
I have set up AdGuard Home correctly and [configured clients to
use it](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients).
(Use the
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
for help with installing and configuring clients.)
'required': true
'id': 'prerequisites'
'type': 'checkboxes'
- 'attributes':
'description': 'On which operating system type does the issue occur?'
'label': 'Operating system type'
'description': 'On which Platform does the issue occur?'
'label': 'Platform (OS and CPU architecture)'
'options':
- 'FreeBSD'
- 'Linux, OpenWrt'
- 'Linux, Other (please mention the version in the description)'
- 'macOS (aka Darwin)'
- 'OpenBSD'
- 'Windows'
- 'Other (please mention in the description)'
- 'Darwin (aka macOS)/AMD64 (aka x86_64)'
- 'Darwin (aka macOS)/ARM64'
- 'FreeBSD/386'
- 'FreeBSD/AMD64 (aka x86_64)'
- 'FreeBSD/ARM64'
- 'FreeBSD/ARMv5'
- 'FreeBSD/ARMv6'
- 'FreeBSD/ARMv7'
- 'Linux/386'
- 'Linux/AMD64 (aka x86_64)'
- 'Linux/ARM64'
- 'Linux/ARMv5'
- 'Linux/ARMv6'
- 'Linux/ARMv7'
- 'Linux/MIPS LE'
- 'Linux/MIPS'
- 'Linux/MIPS64 LE'
- 'Linux/MIPS64'
- 'Linux/PPC64 LE'
- 'OpenBSD/AMD64 (aka x86_64)'
- 'OpenBSD/ARM64'
- 'Windows/386'
- 'Windows/AMD64 (aka x86_64)'
- 'Windows/ARM64'
- 'Custom (please mention in the description)'
'id': 'os'
'type': 'dropdown'
'validations':
'required': true
- 'attributes':
'description': 'On which CPU architecture does the issue occur?'
'label': 'CPU architecture'
'options':
- 'AMD64'
- 'x86'
- '64-bit ARM'
- 'ARMv5'
- 'ARMv6'
- 'ARMv7'
- '64-bit MIPS'
- '64-bit MIPS LE'
- '32-bit MIPS'
- '32-bit MIPS LE'
- '64-bit PowerPC LE'
- 'Other (please mention in the description)'
'id': 'arch'
'type': 'dropdown'
'validations':
'required': true
- 'attributes':
'description': 'How did you install AdGuard Home?'
'label': 'Installation'
@@ -63,7 +69,7 @@
- 'GitHub releases or script from README'
- 'Docker'
- 'Snapcraft'
- 'Custom port'
- 'Custom package (OpenWrt, HomeAssistant, etc; please mention in the description)'
- 'Other (please mention in the description)'
'id': 'install'
'type': 'dropdown'
@@ -89,21 +95,55 @@
'validations':
'required': true
- 'attributes':
'description': 'Please describe the bug'
'label': 'Description'
'description': >
Please describe what you did. An `nslookup` or a `dig` command is
the best way. For crashes, please provide a full failure log.
'label': 'Action'
'value': |
#### What did you do?
#### Expected result
#### Actual result
#### Screenshots (if applicable)
#### Additional information
'id': 'description'
```sh
nslookup -debug -type=a 'www.example.com' '$YOUR_AGH_ADDRESS'
```
'id': 'failing_action'
'type': 'textarea'
'validations':
'required': true
'description': 'File a bug report'
- 'attributes':
'description': >
What did you expect to see? Please add a description and/or
screenshots, if applicable.
'label': 'Expected result'
'placeholder': >
What did you expect to see?
'id': 'expected'
'type': 'textarea'
'validations':
'required': true
- 'attributes':
'description': >
What happened instead? Please add a description and/or screenshots,
if applicable.
'label': 'Actual result'
'placeholder': >
What did you see instead?
'id': 'result'
'type': 'textarea'
'validations':
'required': true
- 'attributes':
'description': >
Please add additional information, such as non-standard OS or port,
here. You can also put screenshots here, if applicable. For
example, it is better to copy and paste text from a terminal instead
of posting a screenshot of the terminal.
'label': 'Additional information and/or screenshots'
'placeholder': >
Additional OS information, screenshots of the UI, etc.
'id': 'additional'
'type': 'textarea'
'validations':
'required': false
'description': >
Open a bug report. Please do not open bug reports for questions or help
with configuring clients. If you want to ask for help, use the Discussions
section.
'name': 'Bug'

View File

@@ -23,19 +23,32 @@
'id': 'prerequisites'
'type': 'checkboxes'
- 'attributes':
'description': 'Please describe the request'
'label': 'Description'
'value': |
#### What problem are you trying to solve?
#### Proposed solution
#### Alternatives considered
#### Additional information
'id': 'description'
'description': 'Please describe the problem you are trying to solve'
'label': 'The problem'
'placeholder': >
Please describe the problem you are trying to solve
'id': 'problem'
'type': 'textarea'
'validations':
'required': true
- 'attributes':
'description': 'What feature are you proposing to solve this problem?'
'label': 'Proposed solution'
'placeholder': >
What feature are you proposing to solve this problem?
'id': 'proposed_solution'
'type': 'textarea'
'validations':
'required': true
- 'attributes':
'label': 'Alternatives considered and additional information'
'placeholder': >
Are there any other ways to solve the problem?
'id': 'additional'
'type': 'textarea'
'validations':
'required': false
'description': 'Suggest a feature or an enhancement for AdGuard Home'
'labels':
- 'feature request'
'name': 'Feature request or enhancement'

20
.github/PULL_REQUEST_TEMPLATE vendored Normal file
View File

@@ -0,0 +1,20 @@
Before submitting a PR please make sure that:
1. You have discussed your solution in an issue and have got an
approval from a maintainer.
2. This isn't a localization fix; please send those to our
[CrowdIn](https://crowdin.com/project/adguard-applications/en#/adguard-home)
page.
3. Your code follows our
[code guidelines](https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md).
Add a short description here. The description should include:
1. Which issue this PR closes (`Closes #NNNN.`) or updates (`Updates
#NNNN.`).
2. A short description of how the change achieves that.
Do not forget to remove these instructions.

View File

@@ -0,0 +1,18 @@
'name': 'potential-duplicates'
'on':
'issues':
'types':
- 'opened'
'jobs':
'run':
'runs-on': 'ubuntu-latest'
'steps':
- 'uses': 'wow-actions/potential-duplicates@v1'
'with':
'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'
'state': 'all'
'threshold': 0.6
'comment': |
Potential duplicates: {{#issues}}
* [#{{ number }}] {{ title }} ({{ accuracy }}%)
{{/issues}}

View File

@@ -14,21 +14,87 @@ and this project adheres to
<!--
## [v0.108.0] - TBA
## [v0.107.32] - 2023-06-28 (APPROX.)
## [v0.107.33] - 2023-06-28 (APPROX.)
See also the [v0.107.32 GitHub milestone][ms-v0.107.32].
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
[ms-v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
NOTE: Add new changes BELOW THIS COMMENT.
-->
### Added
- The new HTTP API, `GET /control/querylog/export`, which can be used to
export query log items. See `openapi/openapi.yaml` for the full description
([#3389]).
- The ability to set inactivity periods for filtering blocked services in the
configuration file ([#951]). The UI changes are coming in the upcoming
releases.
- The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API
([#1577]).
### Changed
#### Configuration Changes
In this release, the schema version has changed from 20 to 21.
- Property `dns.blocked_services`, which in schema versions 20 and earlier used
to be a list containing ids of blocked services, is now an object containing
ids and schedule for blocked services:
```yaml
# BEFORE:
'blocked_services':
- id_1
- id_2
# AFTER:
'blocked_services':
'ids':
- id_1
- id_2
'schedule':
'time_zone': 'Local'
'sun':
'start': '0s'
'end': '24h'
'mon':
'start': '10m'
'end': '23h30m'
'tue':
'start': '20m'
'end': '23h'
'wed':
'start': '30m'
'end': '22h30m'
'thu':
'start': '40m'
'end': '22h'
'fri':
'start': '50m'
'end': '21h30m'
'sat':
'start': '1h'
'end': '21h'
```
To rollback this change, replace `dns.blocked_services` object with the list
of ids of blocked services and change the `schema_version` back to `20`.
### Fixed
- Queries with the question-section target `.`, for example `NS .`, are now
counted in the statistics and correctly shown in the query log ([#5910]).
- Safe Search not working with `AAAA` queries for domains that don't have `AAAA`
records ([#5913]).
[#951]: https://github.com/AdguardTeam/AdGuardHome/issues/951
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
[#3389]: https://github.com/AdguardTeam/AdGuardHome/issues/3389
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
@@ -40,8 +106,8 @@ NOTE: Add new changes ABOVE THIS COMMENT.
### Fixed
- DNSCrypt upstream not resetting the client and resolver information on
dialing errors ([#5872]).
- DNSCrypt upstream not resetting the client and resolver information on
dialing errors ([#5872]).

View File

@@ -1,5 +1,8 @@
---
!include test.yaml
---
!include release.yaml
---
!include snapcraft.yaml
---
!include test.yaml

View File

@@ -1,348 +1,290 @@
---
'version': 2
'plan':
'project-key': 'AGH'
'key': 'AGHBSNAPSPECS'
'name': 'AdGuard Home - Build and publish release'
'project-key': 'AGH'
'key': 'AGHBSNAPSPECS'
'name': 'AdGuard Home - Build and publish release'
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'stages':
- 'Build frontend':
'manual': false
'final': false
'jobs':
- 'Build frontend'
- 'Build frontend':
'manual': false
'final': false
'jobs':
- 'Build frontend'
- 'Make release':
'manual': false
'final': false
'jobs':
- 'Make release'
- 'Make release':
'manual': false
'final': false
'jobs':
- 'Make release'
- 'Make and publish docker':
'manual': false
'final': false
'jobs':
- 'Make and publish docker'
- 'Make and publish docker':
'manual': false
'final': false
'jobs':
- 'Make and publish docker'
- 'Publish to static storage':
'manual': false
'final': false
'jobs':
- 'Publish to static storage'
- 'Publish to static storage':
'manual': false
'final': false
'jobs':
- 'Publish to static storage'
- 'Publish to Snapstore':
'manual': false
'final': false
'jobs':
- 'Publish to Snapstore'
- 'Publish to GitHub Releases':
'manual': false
'final': false
'jobs':
- 'Publish to GitHub Releases'
- 'Publish to GitHub Releases':
'manual': false
'final': false
'jobs':
- 'Publish to GitHub Releases'
'Build frontend':
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'key': 'BF'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'key': 'BF'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
make js-deps js-build
'artifacts':
- 'name': 'AdGuardHome frontend'
'pattern': 'build*/**'
'shared': true
'required': true
'requirements':
- 'adg-docker': 'true'
make js-deps js-build
'artifacts':
- 'name': 'AdGuardHome frontend'
'pattern': 'build/**'
'shared': true
'required': true
'requirements':
- 'adg-docker': 'true'
'Make release':
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
'key': 'MR'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
'key': 'MR'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
# Run the build with the specified channel.
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
| awk '{ gsub(/\\n/, "\n"); print; }'\
| gpg --import --batch --yes
# Run the build with the specified channel.
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
| awk '{ gsub(/\\n/, "\n"); print; }'\
| gpg --import --batch --yes
make\
CHANNEL=${bamboo.channel}\
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
FRONTEND_PREBUILT=1\
PARALLELISM=1\
VERBOSE=2\
build-release
# TODO(a.garipov): Use more fine-grained artifact rules.
'artifacts':
- 'name': 'AdGuardHome dists'
'pattern': 'dist/**'
'shared': true
'required': true
'requirements':
- 'adg-docker': 'true'
make\
CHANNEL=${bamboo.channel}\
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
FRONTEND_PREBUILT=1\
PARALLELISM=1\
VERBOSE=2\
build-release
# TODO(a.garipov): Use more fine-grained artifact rules.
'artifacts':
- 'name': 'AdGuardHome dists'
'pattern': 'dist/**'
'shared': true
'required': true
'requirements':
- 'adg-docker': 'true'
'Make and publish docker':
'key': 'MPD'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'key': 'MPD'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
COMMIT="${bamboo.repository.revision.number}"
export COMMIT
readonly COMMIT
COMMIT="${bamboo.repository.revision.number}"
export COMMIT
readonly COMMIT
# Explicitly checkout the revision that we need.
git checkout "$COMMIT"
# Explicitly checkout the revision that we need.
git checkout "$COMMIT"
# Install Qemu, create builder.
docker version -f '{{ .Server.Experimental }}'
docker buildx rm buildx-builder || :
docker buildx create --name buildx-builder --driver docker-container\
--use
docker buildx inspect --bootstrap
# Install Qemu, create builder.
docker version -f '{{ .Server.Experimental }}'
docker buildx rm buildx-builder || :
docker buildx create --name buildx-builder --driver docker-container\
--use
docker buildx inspect --bootstrap
# Login to DockerHub.
docker login -u="${bamboo.dockerHubUsername}"\
-p="${bamboo.dockerHubPassword}"
# Login to DockerHub.
docker login -u="${bamboo.dockerHubUsername}"\
-p="${bamboo.dockerHubPassword}"
# Boot the builder.
docker buildx inspect --bootstrap
# Boot the builder.
docker buildx inspect --bootstrap
# Print Docker info.
docker info
# Print Docker info.
docker info
# Prepare and push the build.
env\
CHANNEL="${bamboo.channel}"\
DIST_DIR='dist'\
DOCKER_IMAGE_NAME='adguard/adguardhome'\
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
VERBOSE='1'\
sh ./scripts/make/build-docker.sh
'environment':
DOCKER_CLI_EXPERIMENTAL=enabled
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
# Prepare and push the build.
env\
CHANNEL="${bamboo.channel}"\
DIST_DIR='dist'\
DOCKER_IMAGE_NAME='adguard/adguardhome'\
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
VERBOSE='1'\
sh ./scripts/make/build-docker.sh
'environment':
DOCKER_CLI_EXPERIMENTAL=enabled
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'Publish to static storage':
'key': 'PUB'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'key': 'PUB'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
cd ./dist/
cd ./dist/
CHANNEL="${bamboo.channel}"
export CHANNEL
CHANNEL="${bamboo.channel}"
export CHANNEL
../bamboo-deploy-publisher/deploy.sh adguard-home-"$CHANNEL"
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'Publish to Snapstore':
'docker':
'image': '${bamboo.dockerGo}'
'key': 'PTS'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
cd ./dist/
channel="${bamboo.channel}"
readonly channel
case "$channel"
in
('release')
snapchannel='candidate'
;;
('beta')
snapchannel='beta'
;;
('edge')
snapchannel='edge'
;;
(*)
echo "invalid channel '$channel'"
exit 1
;;
esac
env\
SNAPCRAFT_CHANNEL="$snapchannel"\
SNAPCRAFT_EMAIL="${bamboo.snapcraftEmail}"\
SNAPCRAFT_STORE_CREDENTIALS="${bamboo.snapcraftMacaroonPassword}"\
../bamboo-deploy-publisher/deploy.sh adguard-home-snap
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
../bamboo-deploy-publisher/deploy.sh adguard-home-"$CHANNEL"
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'Publish to GitHub Releases':
'key': 'PTGR'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'key': 'PTGR'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
channel="${bamboo.channel}"
readonly channel
channel="${bamboo.channel}"
readonly channel
if [ "$channel" != 'release' ] && [ "${channel}" != 'beta' ]
then
echo "don't publish to GitHub Releases for this channel"
if [ "$channel" != 'release' ] && [ "${channel}" != 'beta' ]
then
echo "don't publish to GitHub Releases for this channel"
exit 0
fi
exit 0
fi
cd ./dist/
cd ./dist/
env\
GITHUB_TOKEN="${bamboo.githubPublicRepoPassword}"\
../bamboo-deploy-publisher/deploy.sh adguard-home-github
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
env\
GITHUB_TOKEN="${bamboo.githubPublicRepoPassword}"\
../bamboo-deploy-publisher/deploy.sh adguard-home-github
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'triggers':
# Don't use minute values that end with a zero or a five as these are often used
# in CI and so resources during these minutes can be quite busy.
- 'cron': '0 42 13 ? * MON-FRI *'
# Don't use minute values that end with a zero or a five as these are often
# used in CI and so resources during these minutes can be quite busy.
- 'cron': '0 42 13 ? * MON-FRI *'
'branches':
'create': 'manually'
'delete':
'after-deleted-days': 1
'after-inactive-days': 30
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and publish release'
'link-to-jira': true
'create': 'manually'
'delete':
'after-deleted-days': 1
'after-inactive-days': 30
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and publish release'
'link-to-jira': true
'notifications':
- 'events':
- 'plan-completed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa'
- 'events':
- 'plan-completed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa'
'labels': []
'other':
'concurrent-build-plugin': 'system-default'
'concurrent-build-plugin': 'system-default'
'branch-overrides':
# beta-vX.Y branches are the branches into which the commits that are needed to
# release a new patch version are initially cherry-picked.
- '^beta-v[0-9]+\.[0-9]+':
# Build betas on release branches manually.
'triggers': []
# Set the default release channel on the release branch to beta, as we may
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:6.7'
# release-vX.Y.Z branches are the branches from which the actual final release
# is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
# Disable integration branches for release branches.
'branch-config':
'integration':
'push-on-success': false
'merge-from': 'beta-v0.107'
# Build final releases on release branches manually.
'triggers': []
# Set the default release channel on the final branch to release, as these
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:6.7'
# beta-vX.Y branches are the branches into which the commits that are needed
# to release a new patch version are initially cherry-picked.
- '^beta-v[0-9]+\.[0-9]+':
# Build betas on release branches manually.
'triggers': []
# Set the default release channel on the release branch to beta, as we may
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:6.7'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
# Disable integration branches for release branches.
'branch-config':
'integration':
'push-on-success': false
'merge-from': 'beta-v0.107'
# Build final releases on release branches manually.
'triggers': []
# Set the default release channel on the final branch to release, as these
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:6.7'

211
bamboo-specs/snapcraft.yaml Normal file
View File

@@ -0,0 +1,211 @@
---
# This part of the release build is separate from the one described in
# release.yaml, because the Snapcraft infrastructure is brittle, and timeouts
# during logins and uploads often lead to release blocking.
'version': 2
'plan':
'project-key': 'AGH'
'key': 'AGHSNAP'
'name': 'AdGuard Home - Build and publish Snapcraft release'
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'edge'
'stages':
- 'Download release':
'manual': false
'final': false
'jobs':
- 'Download release'
- 'Build packages':
'manual': false
'final': false
'jobs':
- 'Build packages'
- 'Publish to Snapstore':
'manual': false
'final': false
'jobs':
- 'Publish to Snapstore'
# TODO(a.garipov): Consider using the Artifact Downloader Task if it ever learns
# about plan branches.
'Download release':
'artifacts':
- 'name': 'i386_binary'
'pattern': 'AdGuardHome_i386'
'shared': true
'required': true
- 'name': 'amd64_binary'
'pattern': 'AdGuardHome_amd64'
'shared': true
'required': true
- 'name': 'armhf_binary'
'pattern': 'AdGuardHome_armhf'
'shared': true
'required': true
- 'name': 'arm64_binary'
'pattern': 'AdGuardHome_arm64'
'shared': true
'required': true
'docker':
'image': '${bamboo.dockerGo}'
'key': 'DR'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
CHANNEL="${bamboo.channel}"\
VERBOSE='1'\
sh ./scripts/snap/download.sh
'requirements':
- 'adg-docker': 'true'
'Build packages':
'artifact-subscriptions':
- 'artifact': 'i386_binary'
- 'artifact': 'amd64_binary'
- 'artifact': 'armhf_binary'
- 'artifact': 'arm64_binary'
'artifacts':
- 'name': 'i386_snap'
'pattern': 'AdGuardHome_i386.snap'
'shared': true
'required': true
- 'name': 'amd64_snap'
'pattern': 'AdGuardHome_amd64.snap'
'shared': true
'required': true
- 'name': 'armhf_snap'
'pattern': 'AdGuardHome_armhf.snap'
'shared': true
'required': true
- 'name': 'arm64_snap'
'pattern': 'AdGuardHome_arm64.snap'
'shared': true
'required': true
'docker':
'image': '${bamboo.dockerGo}'
'key': 'BP'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
VERBOSE='1'\
sh ./scripts/snap/build.sh
'requirements':
- 'adg-docker': 'true'
'Publish to Snapstore':
'artifact-subscriptions':
- 'artifact': 'i386_snap'
- 'artifact': 'amd64_snap'
- 'artifact': 'armhf_snap'
- 'artifact': 'arm64_snap'
'docker':
'image': '${bamboo.dockerGo}'
'key': 'PTS'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
SNAPCRAFT_CHANNEL="${bamboo.snapcraftChannel}"\
SNAPCRAFT_STORE_CREDENTIALS="${bamboo.snapcraftMacaroonPassword}"\
VERBOSE='1'\
sh ./scripts/snap/upload.sh
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'triggers':
# Don't use minute values that end with a zero or a five as these are often
# used in CI and so resources during these minutes can be quite busy.
#
# NOTE: The time is chosen to be exactly one hour after the main release
# build as defined as in release.yaml.
- 'cron': '0 42 14 ? * MON-FRI *'
'branches':
'create': 'manually'
'delete':
'after-deleted-days': 1
'after-inactive-days': 30
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and publish Snapcraft release'
'link-to-jira': true
'notifications':
- 'events':
- 'plan-completed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa'
'labels': []
'other':
'concurrent-build-plugin': 'system-default'
'branch-overrides':
# beta-vX.Y branches are the branches into which the commits that are needed
# to release a new patch version are initially cherry-picked.
- '^beta-v[0-9]+\.[0-9]+':
# Build betas on release branches manually.
'triggers': []
# Set the default release channel on the release branch to beta, as we may
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'beta'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
# Disable integration branches for release branches.
'branch-config':
'integration':
'push-on-success': false
'merge-from': 'beta-v0.107'
# Build final releases on release branches manually.
'triggers': []
# Set the default release channel on the final branch to release, as these
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'candidate'

View File

@@ -1,64 +1,64 @@
---
'version': 2
'plan':
'project-key': 'AGH'
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'project-key': 'AGH'
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
'dockerGo': 'adguard/golang-ubuntu:6.7'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'stages':
- 'Tests':
'manual': false
'final': false
'jobs':
- 'Test'
- 'Tests':
'manual': false
'final': false
'jobs':
- 'Test'
'Test':
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
'key': 'TEST'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
'key': 'TEST'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
set -e -f -u -x
make VERBOSE=1 ci go-tools lint
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
make VERBOSE=1 ci go-tools lint
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'branches':
'create': 'for-pull-request'
'delete':
'after-deleted-days': 1
'after-inactive-days': 5
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and run tests'
'link-to-jira': true
'create': 'for-pull-request'
'delete':
'after-deleted-days': 1
'after-inactive-days': 5
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and run tests'
'link-to-jira': true
'notifications':
- 'events':
- 'plan-status-changed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo'
- 'events':
- 'plan-status-changed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo'
'labels': []
'other':
'concurrent-build-plugin': 'system-default'
'concurrent-build-plugin': 'system-default'

14
go.mod
View File

@@ -4,10 +4,11 @@ go 1.19
require (
github.com/AdguardTeam/dnsproxy v0.50.2
github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/golibs v0.13.3
github.com/AdguardTeam/urlfilter v0.16.1
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.7
github.com/bluele/gcache v0.0.2
github.com/digineo/go-ipset/v2 v2.2.1
github.com/dimfeld/httptreemux/v5 v5.5.0
github.com/fsnotify/fsnotify v1.6.0
@@ -27,13 +28,13 @@ require (
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.54
github.com/quic-go/quic-go v0.35.1
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
github.com/ti-mo/netfilter v0.5.0
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.9.0
golang.org/x/crypto v0.10.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0
golang.org/x/sys v0.8.0
golang.org/x/net v0.11.0
golang.org/x/sys v0.9.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0
@@ -44,7 +45,6 @@ require (
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
@@ -61,6 +61,6 @@ require (
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.9.3 // indirect
)

28
go.sum
View File

@@ -2,8 +2,8 @@ github.com/AdguardTeam/dnsproxy v0.50.2 h1:p1471SsMZ6SMo7T51Olw4aNluahvMwSLMorwx
github.com/AdguardTeam/dnsproxy v0.50.2/go.mod h1:CQhZTkqC8X0ID6glrtyaxgqRRdiYfn1gJulC1cZ5Dn8=
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.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
github.com/AdguardTeam/golibs v0.13.3 h1:RT3QbzThtaLiFLkIUDS6/hlGEXrh0zYvdf4bd7UWpGo=
github.com/AdguardTeam/golibs v0.13.3/go.mod h1:wkJ6EUsN4np/9Gp7+9QeooY9E2U2WCLJYAioLCzkHsI=
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/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
@@ -113,17 +113,13 @@ github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
github.com/ti-mo/netfilter v0.5.0 h1:MZmsUw5bFRecOb0AeyjOPxTHg4UxYzyEs0Ek/6Lxoy8=
github.com/ti-mo/netfilter v0.5.0/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8=
@@ -138,8 +134,8 @@ go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -156,8 +152,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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
@@ -181,16 +177,16 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
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.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.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.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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=

View File

@@ -53,14 +53,14 @@ func (s *Server) beforeRequestHandler(
// getClientRequestFilteringSettings looks up client filtering settings using
// the client's IP address and ID, if any, from dctx.
func (s *Server) getClientRequestFilteringSettings(dctx *dnsContext) *filtering.Settings {
setts := s.dnsFilter.GetConfig()
setts := s.dnsFilter.Settings()
setts.ProtectionEnabled = dctx.protectionEnabled
if s.conf.FilterHandler != nil {
ip, _ := netutil.IPAndPortFromAddr(dctx.proxyCtx.Addr)
s.conf.FilterHandler(ip, dctx.clientID, &setts)
s.conf.FilterHandler(ip, dctx.clientID, setts)
}
return &setts
return setts
}
// filterDNSRequest applies the dnsFilter and sets dctx.proxyCtx.Res if the

View File

@@ -57,16 +57,13 @@ func (s *Server) genDNSFilterMessage(
return s.genBlockedHost(req, s.conf.SafeBrowsingBlockHost, dctx)
case filtering.FilteredParental:
return s.genBlockedHost(req, s.conf.ParentalBlockHost, dctx)
case filtering.FilteredSafeSearch:
// If Safe Search generated the necessary IP addresses, use them.
// Otherwise, if there were no errors, there are no addresses for the
// requested IP version, so produce a NODATA response.
return s.genResponseWithIPs(req, ipsFromRules(res.Rules))
default:
// If the query was filtered by Safe Search, filtering also must return
// the IP addresses that must be used in response. Return them
// regardless of the filtering method.
ips := ipsFromRules(res.Rules)
if res.Reason == filtering.FilteredSafeSearch && len(ips) > 0 {
return s.genResponseWithIPs(req, ips)
}
return s.genForBlockingMode(req, ips)
return s.genForBlockingMode(req, ipsFromRules(res.Rules))
}
}

View File

@@ -123,7 +123,10 @@ func (s *Server) updateStats(
pctx := ctx.proxyCtx
e := stats.Entry{}
e.Domain = strings.ToLower(pctx.Req.Question[0].Name)
e.Domain = e.Domain[:len(e.Domain)-1] // remove last "."
if e.Domain != "." {
// Remove last ".", but save the domain as is for "." queries.
e.Domain = e.Domain[:len(e.Domain)-1]
}
if clientID := ctx.clientID; clientID != "" {
e.Client = clientID

View File

@@ -46,6 +46,10 @@ type testStats struct {
// Update implements the [stats.Interface] interface for *testStats.
func (l *testStats) Update(e stats.Entry) {
if e.Domain == "" {
return
}
l.lastEntry = e
}
@@ -54,9 +58,12 @@ func (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {
return true
}
func TestProcessQueryLogsAndStats(t *testing.T) {
func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
const domain = "example.com."
testCases := []struct {
name string
domain string
proto proxy.Proto
addr net.Addr
clientID string
@@ -67,6 +74,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult stats.Result
}{{
name: "success_udp",
domain: domain,
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -77,6 +85,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_tls_clientid",
domain: domain,
proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "cli42",
@@ -87,6 +96,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_tls",
domain: domain,
proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -97,6 +107,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_quic",
domain: domain,
proto: proxy.ProtoQUIC,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -107,6 +118,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_https",
domain: domain,
proto: proxy.ProtoHTTPS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -117,6 +129,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_dnscrypt",
domain: domain,
proto: proxy.ProtoDNSCrypt,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -127,6 +140,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered,
}, {
name: "success_udp_filtered",
domain: domain,
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -137,6 +151,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RFiltered,
}, {
name: "success_udp_sb",
domain: domain,
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -147,6 +162,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RSafeBrowsing,
}, {
name: "success_udp_ss",
domain: domain,
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -157,6 +173,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RSafeSearch,
}, {
name: "success_udp_pc",
domain: domain,
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "",
@@ -165,6 +182,17 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
wantCode: resultCodeSuccess,
reason: filtering.FilteredParental,
wantStatResult: stats.RParental,
}, {
name: "success_udp_pc_empty_fqdn",
domain: ".",
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 5}, Port: 1234},
clientID: "",
wantLogProto: "",
wantStatClient: "1.2.3.5",
wantCode: resultCodeSuccess,
reason: filtering.FilteredParental,
wantStatResult: stats.RParental,
}}
ups, err := upstream.AddressToUpstream("1.1.1.1", nil)
@@ -181,7 +209,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := &dns.Msg{
Question: []dns.Question{{
Name: "example.com.",
Name: tc.domain,
}},
}
pctx := &proxy.DNSContext{

View File

@@ -3,8 +3,10 @@ package filtering
import (
"encoding/json"
"net/http"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
"golang.org/x/exp/slices"
@@ -44,6 +46,15 @@ func initBlockedServices() {
log.Debug("filtering: initialized %d services", l)
}
// BlockedServices is the configuration of blocked services.
type BlockedServices struct {
// Schedule is blocked services schedule for every day of the week.
Schedule *schedule.Weekly `yaml:"schedule"`
// IDs is the names of blocked services.
IDs []string `yaml:"ids"`
}
// BlockedSvcKnown returns true if a blocked service ID is known.
func BlockedSvcKnown(s string) (ok bool) {
_, ok = serviceRules[s]
@@ -52,15 +63,22 @@ func BlockedSvcKnown(s string) (ok bool) {
}
// ApplyBlockedServices - set blocked services settings for this DNS request
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
func (d *DNSFilter) ApplyBlockedServices(setts *Settings) {
d.confLock.RLock()
defer d.confLock.RUnlock()
setts.ServicesRules = []ServiceEntry{}
if list == nil {
d.confLock.RLock()
defer d.confLock.RUnlock()
list = d.Config.BlockedServices
bsvc := d.BlockedServices
// TODO(s.chzhen): Use startTime from [dnsforward.dnsContext].
if !bsvc.Schedule.Contains(time.Now()) {
d.ApplyBlockedServicesList(setts, bsvc.IDs)
}
}
// ApplyBlockedServicesList appends filtering rules to the settings.
func (d *DNSFilter) ApplyBlockedServicesList(setts *Settings, list []string) {
for _, name := range list {
rules, ok := serviceRules[name]
if !ok {
@@ -90,7 +108,7 @@ func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Requ
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
d.confLock.RLock()
list := d.Config.BlockedServices
list := d.Config.BlockedServices.IDs
d.confLock.RUnlock()
_ = aghhttp.WriteJSONResponse(w, r, list)
@@ -106,7 +124,7 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
}
d.confLock.Lock()
d.Config.BlockedServices = list
d.Config.BlockedServices.IDs = list
d.confLock.Unlock()
log.Debug("Updated blocked services list: %d", len(list))

View File

@@ -103,9 +103,9 @@ type Config struct {
Rewrites []*LegacyRewrite `yaml:"rewrites"`
// Names of services to block (globally).
// BlockedServices is the configuration of blocked services.
// Per-client settings can override this configuration.
BlockedServices []string `yaml:"blocked_services"`
BlockedServices *BlockedServices `yaml:"blocked_services"`
// EtcHosts is a container of IP-hostname pairs taken from the operating
// system configuration files (e.g. /etc/hosts).
@@ -298,12 +298,12 @@ func (d *DNSFilter) SetEnabled(enabled bool) {
atomic.StoreUint32(&d.enabled, mathutil.BoolToNumber[uint32](enabled))
}
// GetConfig - get configuration
func (d *DNSFilter) GetConfig() (s Settings) {
// Settings returns filtering settings.
func (d *DNSFilter) Settings() (s *Settings) {
d.confLock.RLock()
defer d.confLock.RUnlock()
return Settings{
return &Settings{
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
SafeSearchEnabled: d.Config.SafeSearchConf.Enabled,
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
@@ -987,16 +987,19 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
return nil, fmt.Errorf("rewrites: preparing: %s", err)
}
bsvcs := []string{}
for _, s := range d.BlockedServices {
if !BlockedSvcKnown(s) {
log.Debug("skipping unknown blocked-service %q", s)
if d.BlockedServices != nil {
bsvcs := []string{}
for _, s := range d.BlockedServices.IDs {
if !BlockedSvcKnown(s) {
log.Debug("skipping unknown blocked-service %q", s)
continue
continue
}
bsvcs = append(bsvcs, s)
}
bsvcs = append(bsvcs, s)
d.BlockedServices.IDs = bsvcs
}
d.BlockedServices = bsvcs
if blockFilters != nil {
err = d.initFiltering(nil, blockFilters)

View File

@@ -416,12 +416,12 @@ type checkHostResp struct {
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("name")
setts := d.GetConfig()
setts := d.Settings()
setts.FilteringEnabled = true
setts.ProtectionEnabled = true
d.ApplyBlockedServices(&setts, nil)
result, err := d.CheckHost(host, dns.TypeA, &setts)
d.ApplyBlockedServices(setts)
result, err := d.CheckHost(host, dns.TypeA, setts)
if err != nil {
aghhttp.Error(
r,

View File

@@ -84,7 +84,7 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
return nil
}
// TODO(a.garipov): Check cnames for cycles on initialisation.
// TODO(a.garipov): Check cnames for cycles on initialization.
cnames := stringutil.NewSet()
host := dReq.Hostname
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {

View File

@@ -161,12 +161,8 @@ func (ss *Default) resetEngine(
// type check
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) {
// CheckHost implements the [filtering.SafeSearch] interface for *Default.
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))
@@ -196,14 +192,10 @@ func (ss *Default) CheckHost(
return filtering.Result{}, err
}
if fltRes != nil {
res = *fltRes
ss.setCacheResult(host, qtype, res)
res = *fltRes
ss.setCacheResult(host, qtype, res)
return res, nil
}
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
return res, nil
}
// searchHost looks up DNS rewrites in the internal DNS filtering engine.
@@ -229,7 +221,11 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
}
// newResult creates Result object from rewrite rule. qtype must be either
// [dns.TypeA] or [dns.TypeAAAA].
// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is never nil, so that the
// empty result is converted into a NODATA response.
//
// TODO(a.garipov): Use the main rewrite result mechanism used in
// [dnsforward.Server.filterDNSRequest].
func (ss *Default) newResult(
rewrite *rules.DNSRewrite,
qtype rules.RRType,
@@ -243,9 +239,10 @@ func (ss *Default) newResult(
}
if rewrite.RRType == qtype {
ip, ok := rewrite.Value.(net.IP)
v := rewrite.Value
ip, ok := v.(net.IP)
if !ok || ip == nil {
return nil, nil
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", v)
}
res.Rules[0].IP = ip
@@ -255,14 +252,14 @@ func (ss *Default) newResult(
host := rewrite.NewCNAME
if host == "" {
return nil, nil
return res, nil
}
ss.log(log.DEBUG, "resolving %q", host)
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
if err != nil {
return nil, err
return nil, fmt.Errorf("resolving cname: %w", err)
}
ss.log(log.DEBUG, "resolved %s", ips)
@@ -276,11 +273,9 @@ func (ss *Default) newResult(
}
res.Rules[0].IP = ip
return res, nil
}
return nil, nil
return res, nil
}
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].

View File

@@ -1,6 +1,7 @@
package safesearch_test
import (
"context"
"net"
"testing"
"time"
@@ -71,6 +72,25 @@ func TestDefault_CheckHost_yandex(t *testing.T) {
}
}
func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
res, err := ss.CheckHost("www.yandex.ru", dns.TypeAAAA)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
// with a nil IP address. This isn't really necessary and should be changed
// once the TODO in [safesearch.Default.newResult] is resolved.
require.Len(t, res.Rules, 1)
assert.Nil(t, res.Rules[0].IP)
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
}
func TestDefault_CheckHost_google(t *testing.T) {
resolver := &aghtest.TestResolver{}
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
@@ -105,6 +125,56 @@ func TestDefault_CheckHost_google(t *testing.T) {
}
}
// testResolver is a [filtering.Resolver] for tests.
//
// TODO(a.garipov): Move to aghtest and use everywhere.
type testResolver struct {
OnLookupIP func(ctx context.Context, network, host string) (ips []net.IP, err error)
}
// type check
var _ filtering.Resolver = (*testResolver)(nil)
// LookupIP implements the [filtering.Resolver] interface for *testResolver.
func (r *testResolver) LookupIP(
ctx context.Context,
network string,
host string,
) (ips []net.IP, err error) {
return r.OnLookupIP(ctx, network, host)
}
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
conf := testConf
conf.CustomResolver = &testResolver{
OnLookupIP: func(_ context.Context, network, host string) (ips []net.IP, err error) {
assert.Equal(t, "ip6", network)
assert.Equal(t, "safe.duckduckgo.com", host)
return nil, nil
},
}
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but
// DuckDuckGo doesn't have a safe-search IPv6 address. The result should be
// the same as the one for Yandex IPv6. That is, a NODATA response.
res, err := ss.CheckHost("www.duckduckgo.com", dns.TypeAAAA)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
// with a nil IP address. This isn't really necessary and should be changed
// once the TODO in [safesearch.Default.newResult] is resolved.
require.Len(t, res.Rules, 1)
assert.Nil(t, res.Rules[0].IP)
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
}
func TestDefault_Update(t *testing.T) {
conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)

View File

@@ -7,6 +7,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/stringutil"
)
@@ -127,14 +128,13 @@ func (cs clientSource) MarshalText() (text []byte, err error) {
// RuntimeClient is a client information about which has been obtained using the
// source described in the Source field.
type RuntimeClient struct {
WHOISInfo *RuntimeClientWHOISInfo
Host string
Source clientSource
}
// WHOIS is the filtered WHOIS data of a client.
WHOIS *whois.Info
// RuntimeClientWHOISInfo is the filtered WHOIS data for a runtime client.
type RuntimeClientWHOISInfo struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
// Host is the host name of a client.
Host string
// Source is the source from which the information about the client has
// been obtained.
Source clientSource
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
@@ -307,18 +308,6 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src clientSource)
return rc.Source
}
func toQueryLogWHOIS(wi *RuntimeClientWHOISInfo) (cw *querylog.ClientWHOIS) {
if wi == nil {
return &querylog.ClientWHOIS{}
}
return &querylog.ClientWHOIS{
City: wi.City,
Country: wi.Country,
Orgname: wi.Orgname,
}
}
// findMultiple is a wrapper around Find to make it a valid client finder for
// the query log. c is never nil; if no information about the client is found,
// it returns an artificial client record by only setting the blocking-related
@@ -352,7 +341,7 @@ func (clients *clientsContainer) clientOrArtificial(
defer func() {
c.Disallowed, c.DisallowedRule = clients.dnsServer.IsBlockedClient(ip, id)
if c.WHOIS == nil {
c.WHOIS = &querylog.ClientWHOIS{}
c.WHOIS = &whois.Info{}
}
}()
@@ -369,7 +358,7 @@ func (clients *clientsContainer) clientOrArtificial(
if ok {
return &querylog.Client{
Name: rc.Host,
WHOIS: toQueryLogWHOIS(rc.WHOISInfo),
WHOIS: rc.WHOIS,
}, false
}
@@ -701,7 +690,7 @@ func (clients *clientsContainer) Update(prev, c *Client) (err error) {
}
// setWHOISInfo sets the WHOIS information for a client.
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWHOISInfo) {
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
clients.lock.Lock()
defer clients.lock.Unlock()
@@ -713,7 +702,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWH
rc, ok := clients.ipToRC[ip]
if ok {
rc.WHOISInfo = wi
rc.WHOIS = wi
log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi)
return
@@ -725,7 +714,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWH
Source: ClientSourceWHOIS,
}
rc.WHOISInfo = wi
rc.WHOIS = wi
clients.ipToRC[ip] = rc
@@ -762,9 +751,9 @@ func (clients *clientsContainer) addHostLocked(
rc.Source = src
} else {
rc = &RuntimeClient{
Host: host,
Source: src,
WHOISInfo: &RuntimeClientWHOISInfo{},
Host: host,
Source: src,
WHOIS: &whois.Info{},
}
clients.ipToRC[ip] = rc

View File

@@ -9,7 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -199,7 +199,7 @@ func TestClients(t *testing.T) {
func TestClientsWHOIS(t *testing.T) {
clients := newClientsContainer()
whois := &RuntimeClientWHOISInfo{
whois := &whois.Info{
Country: "AU",
Orgname: "Example Org",
}
@@ -210,7 +210,7 @@ func TestClientsWHOIS(t *testing.T) {
rc := clients.ipToRC[ip]
require.NotNil(t, rc)
assert.Equal(t, rc.WHOISInfo, whois)
assert.Equal(t, rc.WHOIS, whois)
})
t.Run("existing_auto-client", func(t *testing.T) {
@@ -222,7 +222,7 @@ func TestClientsWHOIS(t *testing.T) {
rc := clients.ipToRC[ip]
require.NotNil(t, rc)
assert.Equal(t, rc.WHOISInfo, whois)
assert.Equal(t, rc.WHOIS, whois)
})
t.Run("can't_set_manually-added", func(t *testing.T) {

View File

@@ -9,6 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
)
// clientJSON is a common structure used by several handlers to deal with
@@ -28,7 +29,8 @@ type clientJSON struct {
// the allowlist.
DisallowedRule *string `json:"disallowed_rule,omitempty"`
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
// WHOIS is the filtered WHOIS data of a client.
WHOIS *whois.Info `json:"whois_info,omitempty"`
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
Name string `json:"name"`
@@ -51,7 +53,7 @@ type clientJSON struct {
}
type runtimeClientJSON struct {
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
WHOIS *whois.Info `json:"whois_info"`
IP netip.Addr `json:"ip"`
Name string `json:"name"`
@@ -78,7 +80,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
for ip, rc := range clients.ipToRC {
cj := runtimeClientJSON{
WHOISInfo: rc.WHOISInfo,
WHOIS: rc.WHOIS,
Name: rc.Host,
Source: rc.Source,
@@ -344,16 +346,16 @@ func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *c
IDs: []string{idStr},
Disallowed: &disallowed,
DisallowedRule: &rule,
WHOISInfo: &RuntimeClientWHOISInfo{},
WHOIS: &whois.Info{},
}
return cj
}
cj = &clientJSON{
Name: rc.Host,
IDs: []string{idStr},
WHOISInfo: rc.WHOISInfo,
Name: rc.Host,
IDs: []string{idStr},
WHOIS: rc.WHOIS,
}
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)

View File

@@ -14,6 +14,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/fastip"
"github.com/AdguardTeam/golibs/errors"
@@ -316,6 +317,11 @@ var config = &configuration{
Yandex: true,
YouTube: true,
},
BlockedServices: &filtering.BlockedServices{
Schedule: schedule.EmptyWeekly(),
IDs: []string{},
},
},
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true,

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -17,6 +18,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@@ -25,7 +27,7 @@ import (
yaml "gopkg.in/yaml.v3"
)
// Default ports.
// Default listening ports.
const (
defaultPortDNS = 53
defaultPortHTTP = 80
@@ -169,13 +171,72 @@ func initDNSServer(
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
}
if config.Clients.Sources.WHOIS {
Context.whois = initWHOIS(&Context.clients)
}
initWHOIS()
return nil
}
// initWHOIS initializes the WHOIS.
//
// TODO(s.chzhen): Consider making configurable.
func initWHOIS() {
const (
// defaultQueueSize is the size of queue of IPs for WHOIS processing.
defaultQueueSize = 255
// defaultTimeout is the timeout for WHOIS requests.
defaultTimeout = 5 * time.Second
// defaultCacheSize is the maximum size of the cache. If it's zero,
// cache size is unlimited.
defaultCacheSize = 10_000
// defaultMaxConnReadSize is an upper limit in bytes for reading from
// net.Conn.
defaultMaxConnReadSize = 64 * 1024
// defaultMaxRedirects is the maximum redirects count.
defaultMaxRedirects = 5
// defaultMaxInfoLen is the maximum length of whois.Info fields.
defaultMaxInfoLen = 250
// defaultIPTTL is the Time to Live duration for cached IP addresses.
defaultIPTTL = 1 * time.Hour
)
Context.whoisCh = make(chan netip.Addr, defaultQueueSize)
var w whois.Interface
if config.Clients.Sources.WHOIS {
w = whois.New(&whois.Config{
DialContext: customDialContext,
ServerAddr: whois.DefaultServer,
Port: whois.DefaultPort,
Timeout: defaultTimeout,
CacheSize: defaultCacheSize,
MaxConnReadSize: defaultMaxConnReadSize,
MaxRedirects: defaultMaxRedirects,
MaxInfoLen: defaultMaxInfoLen,
CacheTTL: defaultIPTTL,
})
} else {
w = whois.Empty{}
}
go func() {
defer log.OnPanic("whois")
for ip := range Context.whoisCh {
info, changed := w.Process(context.Background(), ip)
if info != nil && changed {
Context.clients.setWHOISInfo(ip, info)
}
}
}()
}
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
// a subnet set that matches all locally served networks, see
// [netutil.IsLocallyServed].
@@ -218,9 +279,7 @@ func onDNSRequest(pctx *proxy.DNSContext) {
Context.rdns.Begin(ip)
}
if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
Context.whois.Begin(ip)
}
Context.whoisCh <- ip
}
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
@@ -390,7 +449,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
// pref is a prefix for logging messages around the scope.
const pref = "applying filters"
Context.filters.ApplyBlockedServices(setts, nil)
Context.filters.ApplyBlockedServices(setts)
log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID)
@@ -418,7 +477,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
if svcs == nil {
svcs = []string{}
}
Context.filters.ApplyBlockedServices(setts, svcs)
Context.filters.ApplyBlockedServicesList(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
}
@@ -463,9 +522,7 @@ func startDNSServer() error {
Context.rdns.Begin(ip)
}
if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
Context.whois.Begin(ip)
}
Context.whoisCh <- ip
}
return nil

View File

@@ -57,7 +57,6 @@ type homeContext struct {
queryLog querylog.QueryLog // query log module
dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module
whois *WHOIS // WHOIS module
dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering module
@@ -84,6 +83,9 @@ type homeContext struct {
client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
// whoisCh is the channel for receiving IPs for WHOIS processing.
whoisCh chan netip.Addr
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
tlsCipherIDs []uint16

View File

@@ -3,13 +3,13 @@ package home
import (
"io"
"net/http"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/log"
)
// middlerware is a wrapper function signature.
// middleware is a wrapper function signature.
type middleware func(http.Handler) http.Handler
// withMiddlewares consequently wraps h with all the middlewares.
@@ -75,3 +75,48 @@ func limitRequestBody(h http.Handler) (limited http.Handler) {
h.ServeHTTP(w, rr)
})
}
const (
// defaultWriteTimeout is the maximum duration before timing out writes of
// the response.
defaultWriteTimeout = 60 * time.Second
// longerWriteTimeout is the maximum duration before timing out for APIs
// expecting longer response requests.
longerWriteTimeout = 5 * time.Minute
)
// expectsLongTimeoutRequests shows if this request should use a bigger write
// timeout value. These are exceptions for poorly designed current APIs as
// well as APIs that are designed to expect large files and requests. Remove
// once the new, better APIs are up.
//
// TODO(d.kolyshev): This could be achieved with [http.NewResponseController]
// with go v1.20.
func expectsLongTimeoutRequests(r *http.Request) (ok bool) {
if r.Method != http.MethodGet {
return false
}
return r.URL.Path == "/control/querylog/export"
}
// addWriteTimeout wraps underlying handler h, adding a response write timeout.
func addWriteTimeout(h http.Handler) (limited http.Handler) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var handler http.Handler
if expectsLongTimeoutRequests(r) {
handler = http.TimeoutHandler(h, longerWriteTimeout, "write timeout exceeded")
} else {
handler = http.TimeoutHandler(h, defaultWriteTimeout, "write timeout exceeded")
}
handler.ServeHTTP(w, r)
})
}
// limitHandler wraps underlying handler h with default limits, such as request
// body limit and write timeout.
func limitHandler(h http.Handler) (limited http.Handler) {
return limitRequestBody(addWriteTimeout(h))
}

View File

@@ -22,7 +22,7 @@ import (
)
// currentSchemaVersion is the current schema version.
const currentSchemaVersion = 20
const currentSchemaVersion = 21
// These aliases are provided for convenience.
type (
@@ -94,6 +94,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema17to18,
upgradeSchema18to19,
upgradeSchema19to20,
upgradeSchema20to21,
}
n := 0
@@ -1128,6 +1129,56 @@ func upgradeSchema19to20(diskConf yobj) (err error) {
return nil
}
// upgradeSchema20to21 performs the following changes:
//
// # BEFORE:
// 'dns':
// 'blocked_services':
// - 'svc_name'
//
// # AFTER:
// 'dns':
// 'blocked_services':
// 'ids':
// - 'svc_name'
// 'schedule':
// 'time_zone': 'Local'
func upgradeSchema20to21(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 20 to 21")
diskConf["schema_version"] = 21
const field = "blocked_services"
dnsVal, ok := diskConf["dns"]
if !ok {
return nil
}
dns, ok := dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
blockedVal, ok := dns[field]
if !ok {
return nil
}
services, ok := blockedVal.(yarr)
if !ok {
return fmt.Errorf("unexpected type of blocked: %T", blockedVal)
}
dns[field] = yobj{
"ids": services,
"schedule": yobj{
"time_zone": "Local",
},
}
return nil
}
// TODO(a.garipov): Replace with log.Output when we port it to our logging
// package.
func funcName() string {

View File

@@ -1140,3 +1140,46 @@ func TestUpgradeSchema19to20(t *testing.T) {
assert.Equal(t, 24*time.Hour, ivlVal.Duration)
})
}
func TestUpgradeSchema20to21(t *testing.T) {
const newSchemaVer = 21
testCases := []struct {
in yobj
want yobj
name string
}{{
name: "nothing",
in: yobj{},
want: yobj{
"schema_version": newSchemaVer,
},
}, {
name: "no_clients",
in: yobj{
"dns": yobj{
"blocked_services": yarr{"ok"},
},
},
want: yobj{
"dns": yobj{
"blocked_services": yobj{
"ids": yarr{"ok"},
"schedule": yobj{
"time_zone": "Local",
},
},
},
"schema_version": newSchemaVer,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema20to21(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}

View File

@@ -25,11 +25,13 @@ const (
// readTimeout is the maximum duration for reading the entire request,
// including the body.
readTimeout = 60 * time.Second
// readHdrTimeout is the amount of time allowed to read request headers.
readHdrTimeout = 60 * time.Second
// writeTimeout is the maximum duration before timing out writes of the
// response.
writeTimeout = 60 * time.Second
// response. This limit is overwritten by [addWriteTimeout] middleware.
writeTimeout = 10 * time.Minute
)
type webConfig struct {
@@ -169,7 +171,7 @@ func (web *webAPI) start() {
errs := make(chan error, 2)
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitRequestBody), &http2.Server{})
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitHandler), &http2.Server{})
// Create a new instance, because the Web is not usable after Shutdown.
hostStr := web.conf.BindHost.String()
@@ -254,7 +256,7 @@ func (web *webAPI) tlsServerLoop() {
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(Context.mux, limitRequestBody),
Handler: withMiddlewares(Context.mux, limitHandler),
ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
WriteTimeout: web.conf.WriteTimeout,
@@ -288,7 +290,7 @@ func (web *webAPI) mustStartHTTP3(address string) {
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(Context.mux, limitRequestBody),
Handler: withMiddlewares(Context.mux, limitHandler),
}
log.Debug("web: starting http/3 server")

View File

@@ -1,259 +0,0 @@
package home
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"net/netip"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
)
const (
defaultServer = "whois.arin.net"
defaultPort = "43"
maxValueLength = 250
whoisTTL = 1 * 60 * 60 // 1 hour
)
// WHOIS - module context
type WHOIS struct {
clients *clientsContainer
ipChan chan netip.Addr
// dialContext specifies the dial function for creating unencrypted TCP
// connections.
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// Contains IP addresses of clients
// An active IP address is resolved once again after it expires.
// If IP address couldn't be resolved, it stays here for some time to prevent further attempts to resolve the same IP.
ipAddrs cache.Cache
// TODO(a.garipov): Rewrite to use time.Duration. Like, seriously, why?
timeoutMsec uint
}
// initWHOIS creates the WHOIS module context.
func initWHOIS(clients *clientsContainer) *WHOIS {
w := WHOIS{
timeoutMsec: 5000,
clients: clients,
ipAddrs: cache.New(cache.Config{
EnableLRU: true,
MaxCount: 10000,
}),
dialContext: customDialContext,
ipChan: make(chan netip.Addr, 255),
}
go w.workerLoop()
return &w
}
// If the value is too large - cut it and append "..."
func trimValue(s string) string {
if len(s) <= maxValueLength {
return s
}
return s[:maxValueLength-3] + "..."
}
// isWHOISComment returns true if the string is empty or is a WHOIS comment.
func isWHOISComment(s string) (ok bool) {
return len(s) == 0 || s[0] == '#' || s[0] == '%'
}
// strmap is an alias for convenience.
type strmap = map[string]string
// whoisParse parses a subset of plain-text data from the WHOIS response into
// a string map.
func whoisParse(data string) (m strmap) {
m = strmap{}
var orgname string
lines := strings.Split(data, "\n")
for _, l := range lines {
if isWHOISComment(l) {
continue
}
kv := strings.SplitN(l, ":", 2)
if len(kv) != 2 {
continue
}
k := strings.ToLower(strings.TrimSpace(kv[0]))
v := strings.TrimSpace(kv[1])
if v == "" {
continue
}
switch k {
case "orgname", "org-name":
k = "orgname"
v = trimValue(v)
orgname = v
case "city", "country":
v = trimValue(v)
case "descr", "netname":
k = "orgname"
v = stringutil.Coalesce(orgname, v)
orgname = v
case "whois":
k = "whois"
case "referralserver":
k = "whois"
v = strings.TrimPrefix(v, "whois://")
default:
continue
}
m[k] = v
}
return m
}
// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.
const MaxConnReadSize = 64 * 1024
// Send request to a server and receive the response
func (w *WHOIS) query(ctx context.Context, target, serverAddr string) (data string, err error) {
addr, _, _ := net.SplitHostPort(serverAddr)
if addr == "whois.arin.net" {
target = "n + " + target
}
conn, err := w.dialContext(ctx, "tcp", serverAddr)
if err != nil {
return "", err
}
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
r, err := aghio.LimitReader(conn, MaxConnReadSize)
if err != nil {
return "", err
}
_ = conn.SetReadDeadline(time.Now().Add(time.Duration(w.timeoutMsec) * time.Millisecond))
_, err = conn.Write([]byte(target + "\r\n"))
if err != nil {
return "", err
}
// This use of ReadAll is now safe, because we limited the conn Reader.
var whoisData []byte
whoisData, err = io.ReadAll(r)
if err != nil {
return "", err
}
return string(whoisData), nil
}
// Query WHOIS servers (handle redirects)
func (w *WHOIS) queryAll(ctx context.Context, target string) (string, error) {
server := net.JoinHostPort(defaultServer, defaultPort)
const maxRedirects = 5
for i := 0; i != maxRedirects; i++ {
resp, err := w.query(ctx, target, server)
if err != nil {
return "", err
}
log.Debug("whois: received response (%d bytes) from %s IP:%s", len(resp), server, target)
m := whoisParse(resp)
redir, ok := m["whois"]
if !ok {
return resp, nil
}
redir = strings.ToLower(redir)
_, _, err = net.SplitHostPort(redir)
if err != nil {
server = net.JoinHostPort(redir, defaultPort)
} else {
server = redir
}
log.Debug("whois: redirected to %s IP:%s", redir, target)
}
return "", fmt.Errorf("whois: redirect loop")
}
// Request WHOIS information
func (w *WHOIS) process(ctx context.Context, ip netip.Addr) (wi *RuntimeClientWHOISInfo) {
resp, err := w.queryAll(ctx, ip.String())
if err != nil {
log.Debug("whois: error: %s IP:%s", err, ip)
return nil
}
log.Debug("whois: IP:%s response: %d bytes", ip, len(resp))
m := whoisParse(resp)
wi = &RuntimeClientWHOISInfo{
City: m["city"],
Country: m["country"],
Orgname: m["orgname"],
}
// Don't return an empty struct so that the frontend doesn't get
// confused.
if *wi == (RuntimeClientWHOISInfo{}) {
return nil
}
return wi
}
// Begin - begin requesting WHOIS info
func (w *WHOIS) Begin(ip netip.Addr) {
ipBytes := ip.AsSlice()
now := uint64(time.Now().Unix())
expire := w.ipAddrs.Get(ipBytes)
if len(expire) != 0 {
exp := binary.BigEndian.Uint64(expire)
if exp > now {
return
}
}
expire = make([]byte, 8)
binary.BigEndian.PutUint64(expire, now+whoisTTL)
_ = w.ipAddrs.Set(ipBytes, expire)
log.Debug("whois: adding %s", ip)
select {
case w.ipChan <- ip:
default:
log.Debug("whois: queue is full")
}
}
// workerLoop processes the IP addresses it got from the channel and associates
// the retrieving WHOIS info with a client.
func (w *WHOIS) workerLoop() {
for ip := range w.ipChan {
info := w.process(context.Background(), ip)
if info == nil {
continue
}
w.clients.setWHOISInfo(ip, info)
}
}

View File

@@ -1,152 +0,0 @@
package home
import (
"context"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fakeConn is a mock implementation of net.Conn to simplify testing.
//
// TODO(e.burkov): Search for other places in code where it may be used. Move
// into aghtest then.
type fakeConn struct {
// Conn is embedded here simply to make *fakeConn a net.Conn without
// actually implementing all methods.
net.Conn
data []byte
}
// Write implements net.Conn interface for *fakeConn. It always returns 0 and a
// nil error without mutating the slice.
func (c *fakeConn) Write(_ []byte) (n int, err error) {
return 0, nil
}
// Read implements net.Conn interface for *fakeConn. It puts the content of
// c.data field into b up to the b's capacity.
func (c *fakeConn) Read(b []byte) (n int, err error) {
return copy(b, c.data), io.EOF
}
// Close implements net.Conn interface for *fakeConn. It always returns nil.
func (c *fakeConn) Close() (err error) {
return nil
}
// SetReadDeadline implements net.Conn interface for *fakeConn. It always
// returns nil.
func (c *fakeConn) SetReadDeadline(_ time.Time) (err error) {
return nil
}
// fakeDial is a mock implementation of customDialContext to simplify testing.
func (c *fakeConn) fakeDial(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return c, nil
}
func TestWHOIS(t *testing.T) {
const (
nl = "\n"
data = `OrgName: FakeOrg LLC` + nl +
`City: Nonreal` + nl +
`Country: Imagiland` + nl
)
fc := &fakeConn{
data: []byte(data),
}
w := WHOIS{
timeoutMsec: 5000,
dialContext: fc.fakeDial,
}
resp, err := w.queryAll(context.Background(), "1.2.3.4")
assert.NoError(t, err)
m := whoisParse(resp)
require.NotEmpty(t, m)
assert.Equal(t, "FakeOrg LLC", m["orgname"])
assert.Equal(t, "Imagiland", m["country"])
assert.Equal(t, "Nonreal", m["city"])
}
func TestWHOISParse(t *testing.T) {
const (
city = "Nonreal"
country = "Imagiland"
orgname = "FakeOrgLLC"
whois = "whois.example.net"
)
testCases := []struct {
want strmap
name string
in string
}{{
want: strmap{},
name: "empty",
in: ``,
}, {
want: strmap{},
name: "comments",
in: "%\n#",
}, {
want: strmap{},
name: "no_colon",
in: "city",
}, {
want: strmap{},
name: "no_value",
in: "city:",
}, {
want: strmap{"city": city},
name: "city",
in: `city: ` + city,
}, {
want: strmap{"country": country},
name: "country",
in: `country: ` + country,
}, {
want: strmap{"orgname": orgname},
name: "orgname",
in: `orgname: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_hyphen",
in: `org-name: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_descr",
in: `descr: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_netname",
in: `netname: ` + orgname,
}, {
want: strmap{"whois": whois},
name: "whois",
in: `whois: ` + whois,
}, {
want: strmap{"whois": whois},
name: "referralserver",
in: `referralserver: whois://` + whois,
}, {
want: strmap{},
name: "other",
in: `other: value`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := whoisParse(tc.in)
assert.Equal(t, tc.want, got)
})
}
}

View File

@@ -0,0 +1,39 @@
# AdGuard Home v0.108.0 Changelog DRAFT
This changelog should be merged into the main one once the next API matures
enough.
## [v0.108.0] - TODO
### Added
- The ability to log to stderr using `--logFile=stderr`.
- The new `--web-addr` flag to set the Web UI address in a `host:port` form.
- `SIGHUP` now reloads all configuration from the configuration file ([#5676]).
### Changed
#### New HTTP API
**TODO(a.garipov):** Describe the new API and add a link to the new OpenAPI doc.
#### Other changes
- `-h` is now an alias for `--help` instead of the removed `--host`, see below.
Use `--web-addr=host:port` to set an address on which to serve the Web UI.
### Fixed
- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).
- The order of `-v/--verbose` and `--version` being significant ([#2893]).
### Removed
- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags.
- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address
on which to serve the Web UI. `-h` is now an alias for `--help`, see above.
[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598
[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893
[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902
[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676

View File

@@ -17,20 +17,31 @@ import (
// Main is the entry point of AdGuard Home.
func Main(frontend fs.FS) {
// Initial Configuration
start := time.Now()
// TODO(a.garipov): Set up logging.
// Initial Configuration
cmdName := os.Args[0]
opts, err := parseOptions(cmdName, os.Args[1:])
exitCode, needExit := processOptions(opts, cmdName, err)
if needExit {
os.Exit(exitCode)
}
err = setLog(opts)
check(err)
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
if opts.workDir != "" {
log.Info("changing working directory to %q", opts.workDir)
err = os.Chdir(opts.workDir)
check(err)
}
// Web Service
// TODO(a.garipov): Set up configuration file name.
const confFile = "AdGuardHome.1.yaml"
confMgr, err := configmgr.New(confFile, frontend, start)
confMgr, err := configmgr.New(opts.confFile, frontend, start)
check(err)
web := confMgr.Web()
@@ -42,7 +53,7 @@ func Main(frontend fs.FS) {
check(err)
sigHdlr := newSignalHandler(
confFile,
opts.confFile,
frontend,
start,
web,

39
internal/next/cmd/log.go Normal file
View File

@@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"os"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
)
// syslogServiceName is the name of the AdGuard Home service used for writing
// logs to the system log.
const syslogServiceName = "AdGuardHome"
// setLog sets up the text logging.
//
// TODO(a.garipov): Add parameters from configuration file.
func setLog(opts *options) (err error) {
switch opts.confFile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "syslog":
err = aghos.ConfigureSyslog(syslogServiceName)
if err != nil {
return fmt.Errorf("initializing syslog: %w", err)
}
default:
// TODO(a.garipov): Use the path.
}
if opts.verbose {
log.SetLevel(log.DEBUG)
log.Debug("verbose logging enabled")
}
return nil
}

403
internal/next/cmd/opt.go Normal file
View File

@@ -0,0 +1,403 @@
package cmd
import (
"flag"
"fmt"
"io"
"net/netip"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"golang.org/x/exp/slices"
)
// options contains all command-line options for the AdGuardHome(.exe) binary.
type options struct {
// confFile is the path to the configuration file.
confFile string
// logFile is the path to the log file. Special values:
//
// - "stdout": Write to stdout (the default).
// - "stderr": Write to stderr.
// - "syslog": Write to the system log.
logFile string
// pidFile is the path to the file where to store the PID.
//
// TODO(a.garipov): Use.
pidFile string
// serviceAction is the service control action to perform:
//
// - "install": Installs AdGuard Home as a system service.
// - "uninstall": Uninstalls it.
// - "status": Prints the service status.
// - "start": Starts the previously installed service.
// - "stop": Stops the previously installed service.
// - "restart": Restarts the previously installed service.
// - "reload": Reloads the configuration.
// - "run": This is a special command that is not supposed to be used
// directly it is specified when we register a service, and it indicates
// to the app that it is being run as a service.
//
// TODO(a.garipov): Use.
serviceAction string
// workDir is the path to the working directory. It is applied before all
// other configuration is read, so all relative paths are relative to it.
workDir string
// webAddrs contains the addresses on which to serve the web UI.
//
// TODO(a.garipov): Use.
webAddrs []netip.AddrPort
// checkConfig, if true, instructs AdGuard Home to check the configuration
// file and exit with a corresponding exit code.
//
// TODO(a.garipov): Use.
checkConfig bool
// disableUpdate, if true, prevents AdGuard Home from automatically checking
// for updates.
//
// TODO(a.garipov): Use.
disableUpdate bool
// glinetMode enables the GL-Inet compatibility mode.
//
// TODO(a.garipov): Use.
glinetMode bool
// help, if true, instructs AdGuard Home to print the command-line option
// help message and quit with a successful exit-code.
help bool
// localFrontend, if true, instructs AdGuard Home to use the local frontend
// directory instead of the files compiled into the binary.
//
// TODO(a.garipov): Use.
localFrontend bool
// performUpdate, if true, instructs AdGuard Home to update the current
// binary and restart the service in case it's installed.
//
// TODO(a.garipov): Use.
performUpdate bool
// verbose, if true, instructs AdGuard Home to enable verbose logging.
verbose bool
// version, if true, instructs AdGuard Home to print the version to stdout
// and quit with a successful exit-code. If verbose is also true, print a
// more detailed version description.
version bool
}
// Indexes to help with the [commandLineOptions] initialization.
const (
confFileIdx = iota
logFileIdx
pidFileIdx
serviceActionIdx
workDirIdx
webAddrsIdx
checkConfigIdx
disableUpdateIdx
glinetModeIdx
helpIdx
localFrontend
performUpdateIdx
verboseIdx
versionIdx
)
// commandLineOption contains information about a command-line option: its long
// and, if there is one, short forms, the value type, the description, and the
// default value.
type commandLineOption struct {
defaultValue any
description string
long string
short string
valueType string
}
// commandLineOptions are all command-line options currently supported by
// AdGuard Home.
var commandLineOptions = []*commandLineOption{
confFileIdx: {
// TODO(a.garipov): Remove the ".1" when the new code is ready.
defaultValue: "AdGuardHome.1.yaml",
description: "Path to the config file.",
long: "config",
short: "c",
valueType: "path",
},
logFileIdx: {
defaultValue: "stdout",
description: `Path to log file. Special values include "stdout", "stderr", and "syslog".`,
long: "logfile",
short: "l",
valueType: "path",
},
pidFileIdx: {
defaultValue: "",
description: "Path to the file where to store the PID.",
long: "pidfile",
short: "",
valueType: "path",
},
serviceActionIdx: {
defaultValue: "",
description: `Service control action: "status", "install" (as a service), ` +
`"uninstall" (as a service), "start", "stop", "restart", "reload" (configuration).`,
long: "service",
short: "s",
valueType: "action",
},
workDirIdx: {
defaultValue: "",
description: `Path to the working directory. ` +
`It is applied before all other configuration is read, ` +
`so all relative paths are relative to it.`,
long: "work-dir",
short: "w",
valueType: "path",
},
webAddrsIdx: {
defaultValue: []netip.AddrPort(nil),
description: `Address(es) to serve the web UI on, in the host:port format. ` +
`Can be used multiple times.`,
long: "web-addr",
short: "",
valueType: "host:port",
},
checkConfigIdx: {
defaultValue: false,
description: "Check configuration and quit.",
long: "check-config",
short: "",
valueType: "",
},
disableUpdateIdx: {
defaultValue: false,
description: "Disable automatic update checking.",
long: "no-check-update",
short: "",
valueType: "",
},
glinetModeIdx: {
defaultValue: false,
description: "Run in GL-Inet compatibility mode.",
long: "glinet",
short: "",
valueType: "",
},
helpIdx: {
defaultValue: false,
description: "Print this help message and quit.",
long: "help",
short: "h",
valueType: "",
},
localFrontend: {
defaultValue: false,
description: "Use local frontend directories.",
long: "local-frontend",
short: "",
valueType: "",
},
performUpdateIdx: {
defaultValue: false,
description: "Update the current binary and restart the service in case it's installed.",
long: "update",
short: "",
valueType: "",
},
verboseIdx: {
defaultValue: false,
description: "Enable verbose logging.",
long: "verbose",
short: "v",
valueType: "",
},
versionIdx: {
defaultValue: false,
description: `Print the version to stdout and quit. ` +
`Print a more detailed version description with -v.`,
long: "version",
short: "",
valueType: "",
},
}
// parseOptions parses the command-line options for AdGuardHome.
func parseOptions(cmdName string, args []string) (opts *options, err error) {
flags := flag.NewFlagSet(cmdName, flag.ContinueOnError)
opts = &options{}
for i, fieldPtr := range []any{
confFileIdx: &opts.confFile,
logFileIdx: &opts.logFile,
pidFileIdx: &opts.pidFile,
serviceActionIdx: &opts.serviceAction,
workDirIdx: &opts.workDir,
webAddrsIdx: &opts.webAddrs,
checkConfigIdx: &opts.checkConfig,
disableUpdateIdx: &opts.disableUpdate,
glinetModeIdx: &opts.glinetMode,
helpIdx: &opts.help,
localFrontend: &opts.localFrontend,
performUpdateIdx: &opts.performUpdate,
verboseIdx: &opts.verbose,
versionIdx: &opts.version,
} {
addOption(flags, fieldPtr, commandLineOptions[i])
}
flags.Usage = func() { usage(cmdName, os.Stderr) }
err = flags.Parse(args)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return opts, nil
}
// addOption adds the command-line option described by o to flags using fieldPtr
// as the pointer to the value.
func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) {
switch fieldPtr := fieldPtr.(type) {
case *string:
flags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description)
if o.short != "" {
flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description)
}
case *[]netip.AddrPort:
flags.Func(o.long, o.description, func(s string) (err error) {
addr, err := netip.ParseAddrPort(s)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
*fieldPtr = append(*fieldPtr, addr)
return nil
})
case *bool:
flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description)
if o.short != "" {
flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description)
}
default:
panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr))
}
}
// usage prints a usage message similar to the one printed by package flag but
// taking long vs. short versions into account as well as using more informative
// value hints.
func usage(cmdName string, output io.Writer) {
options := slices.Clone(commandLineOptions)
slices.SortStableFunc(options, func(a, b *commandLineOption) (sortsBefore bool) {
return a.long < b.long
})
b := &strings.Builder{}
_, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName)
for _, o := range options {
writeUsageLine(b, o)
// Use four spaces before the tab to trigger good alignment for both 4-
// and 8-space tab stops.
if shouldIncludeDefault(o.defaultValue) {
_, _ = fmt.Fprintf(b, " \t%s (Default value: %q)\n", o.description, o.defaultValue)
} else {
_, _ = fmt.Fprintf(b, " \t%s\n", o.description)
}
}
_, _ = io.WriteString(output, b.String())
}
// shouldIncludeDefault returns true if this default value should be printed.
func shouldIncludeDefault(v any) (ok bool) {
switch v := v.(type) {
case bool:
return v
case string:
return v != ""
default:
return v == nil
}
}
// writeUsageLine writes the usage line for the provided command-line option.
func writeUsageLine(b *strings.Builder, o *commandLineOption) {
if o.short == "" {
if o.valueType == "" {
_, _ = fmt.Fprintf(b, " --%s\n", o.long)
} else {
_, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType)
}
return
}
if o.valueType == "" {
_, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short)
} else {
_, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType)
}
}
// processOptions decides if AdGuard Home should exit depending on the results
// of command-line option parsing.
func processOptions(
opts *options,
cmdName string,
parseErr error,
) (exitCode int, needExit bool) {
if parseErr != nil {
// Assume that usage has already been printed.
return 2, true
}
if opts.help {
usage(cmdName, os.Stdout)
return 0, true
}
if opts.version {
if opts.verbose {
fmt.Println(version.Verbose())
} else {
fmt.Printf("AdGuard Home %s\n", version.Version())
}
return 0, true
}
return 0, false
}

View File

@@ -54,7 +54,7 @@ func (h *signalHandler) reconfigure() {
status := h.shutdown()
if status != statusSuccess {
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
log.Info("sighdlr: reconfiguring: exiting with status %d", status)
os.Exit(status)
}

View File

@@ -48,7 +48,7 @@ func New(
frontend fs.FS,
start time.Time,
) (m *Manager, err error) {
defer func() { err = errors.Annotate(err, "reading config") }()
defer func() { err = errors.Annotate(err, "reading config: %w") }()
conf := &config{}
f, err := os.Open(fileName)

View File

@@ -78,34 +78,41 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
// Launch the new HTTP service in a separate goroutine to let this handler
// finish and thus, this server to shutdown.
go func() {
defer cancelUpd()
go svc.relaunch(updCtx, cancelUpd, newConf)
}
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
if updErr != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
// relaunch updates the web service in the configuration manager and starts it.
// It is intended to be used as a goroutine.
func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) {
defer log.OnPanic("websvc: relaunching")
defer cancel()
err := svc.confMgr.UpdateWeb(ctx, newConf)
if err != nil {
log.Error("websvc: updating web: %s", err)
return
}
// TODO(a.garipov): Consider better ways to do this.
const maxUpdDur = 5 * time.Second
updStart := time.Now()
var newSvc agh.ServiceWithConfig[*Config]
for newSvc = svc.confMgr.Web(); newSvc == svc; {
if time.Since(updStart) >= maxUpdDur {
log.Error("websvc: failed to update svc after %s", maxUpdDur)
return
}
// TODO(a.garipov): Consider better ways to do this.
const maxUpdDur = 10 * time.Second
updStart := time.Now()
var newSvc agh.ServiceWithConfig[*Config]
for newSvc = svc.confMgr.Web(); newSvc == svc; {
if time.Since(updStart) >= maxUpdDur {
log.Error("websvc: failed to update svc after %s", maxUpdDur)
log.Debug("websvc: waiting for new websvc to be configured")
return
}
time.Sleep(100 * time.Millisecond)
}
log.Debug("websvc: waiting for new websvc to be configured")
time.Sleep(1 * time.Second)
}
updErr = newSvc.Start()
if updErr != nil {
log.Error("websvc: new svc failed to start with error: %s", updErr)
}
}()
err = newSvc.Start()
if err != nil {
log.Error("websvc: new svc failed to start with error: %s", err)
}
}

View File

@@ -139,7 +139,7 @@ func New(c *Config) (svc *Service, err error) {
return svc, nil
}
// newMux returns a new HTTP request multiplexor for the AdGuard Home web
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
// service.
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
mux = httptreemux.NewContextMux()

View File

@@ -1,23 +1,15 @@
package querylog
import "github.com/AdguardTeam/AdGuardHome/internal/whois"
// Client is the information required by the query log to match against clients
// during searches.
type Client struct {
WHOIS *ClientWHOIS `json:"whois,omitempty"`
Name string `json:"name"`
DisallowedRule string `json:"disallowed_rule"`
Disallowed bool `json:"disallowed"`
IgnoreQueryLog bool `json:"-"`
}
// ClientWHOIS is the filtered WHOIS data for the client.
//
// TODO(a.garipov): Merge with home.RuntimeClientWHOISInfo after the
// refactoring is done.
type ClientWHOIS struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
WHOIS *whois.Info `json:"whois,omitempty"`
Name string `json:"name"`
DisallowedRule string `json:"disallowed_rule"`
Disallowed bool `json:"disallowed"`
IgnoreQueryLog bool `json:"-"`
}
// clientCacheKey is the key by which a cached client information is found.

108
internal/querylog/csv.go Normal file
View File

@@ -0,0 +1,108 @@
package querylog
import (
"strconv"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// csvRow is an alias type for csv rows.
type csvRow = [18]string
// csvHeaderRow is a slice of strings with column names for CSV header row.
var csvHeaderRow = csvRow{
"ans_dnssec",
"ans_rcode",
"ans_type",
"ans_value",
"cached",
"client_ip",
"client_id",
"ecs",
"elapsed",
"filter_id",
"filter_rule",
"proto",
"qclass",
"qname",
"qtype",
"reason",
"time",
"upstream",
}
// toCSV returns a slice of strings with entry fields according to the
// csvHeaderRow slice.
func (e *logEntry) toCSV() (out *csvRow) {
var filterID, filterRule string
if e.Result.IsFiltered && len(e.Result.Rules) > 0 {
rule := e.Result.Rules[0]
filterID = strconv.FormatInt(rule.FilterListID, 10)
filterRule = rule.Text
}
aData := ansData(e)
return &csvRow{
strconv.FormatBool(e.AuthenticatedData),
aData.rCode,
aData.typ,
aData.value,
strconv.FormatBool(e.Cached),
e.IP.String(),
e.ClientID,
e.ReqECS,
strconv.FormatFloat(e.Elapsed.Seconds()*1000, 'f', -1, 64),
filterID,
filterRule,
string(e.ClientProto),
e.QClass,
e.QHost,
e.QType,
e.Result.Reason.String(),
e.Time.Format(time.RFC3339Nano),
e.Upstream,
}
}
// csvAnswer is a helper struct for csv row answer fields.
type csvAnswer struct {
rCode string
typ string
value string
}
// ansData returns a map with message answer data.
func ansData(entry *logEntry) (out csvAnswer) {
if len(entry.Answer) == 0 {
return out
}
msg := &dns.Msg{}
if err := msg.Unpack(entry.Answer); err != nil {
log.Debug("querylog: failed to unpack dns msg answer: %v: %s", entry.Answer, err)
return out
}
out.rCode = dns.RcodeToString[msg.Rcode]
if len(msg.Answer) == 0 {
return out
}
rr := msg.Answer[0]
header := rr.Header()
out.typ = dns.TypeToString[header.Rrtype]
// Remove the header string from the answer value since it's mostly
// unnecessary in the log.
out.value = strings.TrimPrefix(rr.String(), header.String())
return out
}

View File

@@ -0,0 +1,73 @@
package querylog
import (
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var testDate = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
func TestLogEntry_toCSV(t *testing.T) {
ans, err := dns.NewRR("www.example.org. IN A 127.0.0.1")
require.NoError(t, err)
ansBytes, err := (&dns.Msg{Answer: []dns.RR{ans}}).Pack()
require.NoError(t, err)
testCases := []struct {
entry *logEntry
want *csvRow
name string
}{{
name: "simple",
entry: &logEntry{
Time: testDate,
QHost: "test.host",
QType: "A",
QClass: "IN",
ReqECS: "",
ClientID: "test-client-id",
ClientProto: ClientProtoDoH,
Upstream: "https://test.upstream:443/dns-query",
Answer: ansBytes,
OrigAnswer: nil,
IP: net.IP{1, 2, 3, 4},
Result: filtering.Result{},
Elapsed: 500 * time.Millisecond,
Cached: false,
AuthenticatedData: false,
},
want: &[18]string{
"false",
"NOERROR",
"A",
"127.0.0.1",
"false",
"1.2.3.4",
"test-client-id",
"",
"500",
"",
"",
"doh",
"IN",
"test.host",
"A",
"NotFilteredNotFound",
"2022-01-01T00:00:00Z",
"https://test.upstream:443/dns-query",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.entry.toCSV())
})
}
}

View File

@@ -1,6 +1,7 @@
package querylog
import (
"encoding/csv"
"encoding/json"
"fmt"
"math"
@@ -14,6 +15,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -62,6 +64,7 @@ func (l *queryLog) initWeb() {
l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog)
l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear)
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/config", l.handleGetQueryLogConfig)
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/export", l.handleQueryLogExport)
l.conf.HTTPRegister(
http.MethodPut,
"/control/querylog/config/update",
@@ -96,6 +99,73 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// exportChunkSize is a size of one search-flush iteration for query log export.
//
// TODO(a.meshkov): Consider making configurable.
const exportChunkSize = 500
// handleQueryLogExport is the handler for the GET /control/querylog/export
// HTTP API.
func (l *queryLog) handleQueryLogExport(w http.ResponseWriter, r *http.Request) {
searchCriteria, err := parseSearchCriteria(r.URL.Query())
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "parsing params: %s", err)
return
}
params := &searchParams{
limit: exportChunkSize,
searchCriteria: searchCriteria,
}
w.Header().Set(httphdr.ContentType, "text/csv; charset=UTF-8; header=present")
w.Header().Set(httphdr.ContentDisposition, "attachment;filename=data.csv")
csvWriter := csv.NewWriter(w)
// Write header.
if err = csvWriter.Write(csvHeaderRow[:]); err != nil {
http.Error(w, "writing csv header", http.StatusInternalServerError)
return
}
csvWriter.Flush()
var entries []*logEntry
for {
func() {
l.confMu.RLock()
defer l.confMu.RUnlock()
entries, _ = l.search(params)
}()
if len(entries) == 0 {
break
}
params.offset += params.limit
for _, entry := range entries {
row := entry.toCSV()
if err = csvWriter.Write(row[:]); err != nil {
// TODO(a.garipov): Set Trailer X-Error header.
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv record", err)
return
}
}
csvWriter.Flush()
}
if err = csvWriter.Error(); err != nil {
// TODO(a.garipov): Set Trailer X-Error header.
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv", err)
}
}
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
// API.
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
@@ -360,6 +430,17 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
p.maxFileScanEntries = 0
}
p.searchCriteria, err = parseSearchCriteria(q)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return p, nil
}
// parseSearchCriteria parses a list of search criteria from the query.
func parseSearchCriteria(q url.Values) (searchCriteria []searchCriterion, err error) {
for _, v := range []struct {
urlField string
ct criterionType
@@ -378,9 +459,9 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
}
if ok {
p.searchCriteria = append(p.searchCriteria, c)
searchCriteria = append(searchCriteria, c)
}
}
return p, nil
return searchCriteria, nil
}

View File

@@ -161,12 +161,15 @@ func (l *queryLog) clear() {
// newLogEntry creates an instance of logEntry from parameters.
func newLogEntry(params *AddParams) (entry *logEntry) {
q := params.Question.Question[0]
qHost := q.Name
if qHost != "." {
qHost = strings.ToLower(q.Name[:len(q.Name)-1])
}
entry = &logEntry{
// TODO(d.kolyshev): Export this timestamp to func params.
Time: time.Now(),
QHost: strings.ToLower(q.Name[:len(q.Name)-1]),
Time: time.Now(),
QHost: qHost,
QType: dns.Type(q.Qtype).String(),
QClass: dns.Class(q.Qclass).String(),

View File

@@ -43,6 +43,7 @@ func TestQueryLog(t *testing.T) {
// Add memory entries.
addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
addEntry(l, "", net.IPv4(1, 1, 1, 5), net.IPv4(2, 2, 2, 5))
type tcAssertion struct {
host string
@@ -59,10 +60,11 @@ func TestQueryLog(t *testing.T) {
name: "all",
sCr: []searchCriterion{},
want: []tcAssertion{
{num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
{num: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)},
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
},
}, {
name: "by_domain_strict",
@@ -104,10 +106,11 @@ func TestQueryLog(t *testing.T) {
value: "2.2.2",
}},
want: []tcAssertion{
{num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
{num: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)},
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
},
}}

View File

@@ -93,3 +93,67 @@ func TestQueryLog_Search_findClient(t *testing.T) {
assert.Equal(t, knownClientName, gotClient.Name)
}
// BenchmarkQueryLog_Search compares the speed of search with limit-offset
// parameters and the one with oldenThan timestamp specified.
func BenchmarkQueryLog_Search(b *testing.B) {
l, err := newQueryLog(Config{
Enabled: true,
RotationIvl: timeutil.Day,
MemSize: 100,
BaseDir: b.TempDir(),
})
require.NoError(b, err)
const (
entNum = 100000
firstPageDomain = "first.example.org"
secondPageDomain = "second.example.org"
)
// Add entries to the log.
for i := 0; i < entNum; i++ {
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
// Write them to the first file.
require.NoError(b, l.flushLogBuffer())
// Add more to the in-memory part of log.
for i := 0; i < entNum; i++ {
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
b.Run("limit_offset", func(b *testing.B) {
params := newSearchParams()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
params.offset += params.limit
_, _ = l.search(params)
}
})
b.Run("timestamp", func(b *testing.B) {
params := newSearchParams()
params.olderThan = time.Now().Add(-1 * time.Hour)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
params.olderThan = params.olderThan.Add(1 * time.Minute)
_, _ = l.search(params)
}
})
// Most recent result, on a MBP15:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/querylog
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
// BenchmarkQueryLog_Search
// BenchmarkQueryLog_Search/limit_offset
// BenchmarkQueryLog_Search/limit_offset-12 547 2066079 ns/op 2325019 B/op 26633 allocs/op
// BenchmarkQueryLog_Search/timestamp
// BenchmarkQueryLog_Search/timestamp-12 1303 2028888 ns/op 2219337 B/op 25194 allocs/op
}

View File

@@ -0,0 +1,220 @@
// Package schedule provides types for scheduling.
package schedule
import (
"fmt"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"gopkg.in/yaml.v3"
)
// Weekly is a schedule for one week. Each day of the week has one range with
// a beginning and an end.
type Weekly struct {
// location is used to calculate the offsets of the day ranges.
location *time.Location
// days are the day ranges of this schedule. The indexes of this array are
// the [time.Weekday] values.
days [7]dayRange
}
// EmptyWeekly creates empty weekly schedule with local time zone.
func EmptyWeekly() (w *Weekly) {
return &Weekly{
location: time.Local,
}
}
// Contains returns true if t is within the corresponding day range of the
// schedule in the schedule's time zone.
func (w *Weekly) Contains(t time.Time) (ok bool) {
t = t.In(w.location)
wd := t.Weekday()
dr := w.days[wd]
// Calculate the offset of the day range.
//
// NOTE: Do not use [time.Truncate] since it requires UTC time zone.
y, m, d := t.Date()
day := time.Date(y, m, d, 0, 0, 0, 0, w.location)
offset := t.Sub(day)
return dr.contains(offset)
}
// type check
var _ yaml.Unmarshaler = (*Weekly)(nil)
// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.
func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
conf := &weeklyConfig{}
err = value.Decode(conf)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
weekly := Weekly{}
weekly.location, err = time.LoadLocation(conf.TimeZone)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
days := []dayConfig{
time.Sunday: conf.Sunday,
time.Monday: conf.Monday,
time.Tuesday: conf.Tuesday,
time.Wednesday: conf.Wednesday,
time.Thursday: conf.Thursday,
time.Friday: conf.Friday,
time.Saturday: conf.Saturday,
}
for i, d := range days {
r := dayRange{
start: d.Start.Duration,
end: d.End.Duration,
}
err = w.validate(r)
if err != nil {
return fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
}
weekly.days[i] = r
}
*w = weekly
return nil
}
// weeklyConfig is the YAML configuration structure of Weekly.
type weeklyConfig struct {
// TimeZone is the local time zone.
TimeZone string `yaml:"time_zone"`
// Days of the week.
Sunday dayConfig `yaml:"sun,omitempty"`
Monday dayConfig `yaml:"mon,omitempty"`
Tuesday dayConfig `yaml:"tue,omitempty"`
Wednesday dayConfig `yaml:"wed,omitempty"`
Thursday dayConfig `yaml:"thu,omitempty"`
Friday dayConfig `yaml:"fri,omitempty"`
Saturday dayConfig `yaml:"sat,omitempty"`
}
// dayConfig is the YAML configuration structure of dayRange.
type dayConfig struct {
Start timeutil.Duration `yaml:"start"`
End timeutil.Duration `yaml:"end"`
}
// maxDayRange is the maximum value for day range end.
const maxDayRange = 24 * time.Hour
// validate returns the day range rounding errors, if any.
func (w *Weekly) validate(r dayRange) (err error) {
defer func() { err = errors.Annotate(err, "bad day range: %w") }()
err = r.validate()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
start := r.start.Truncate(time.Minute)
end := r.end.Truncate(time.Minute)
switch {
case start != r.start:
return fmt.Errorf("start %s isn't rounded to minutes", r.start)
case end != r.end:
return fmt.Errorf("end %s isn't rounded to minutes", r.end)
default:
return nil
}
}
// type check
var _ yaml.Marshaler = (*Weekly)(nil)
// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.
func (w *Weekly) MarshalYAML() (v any, err error) {
return weeklyConfig{
TimeZone: w.location.String(),
Sunday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Sunday].start},
End: timeutil.Duration{Duration: w.days[time.Sunday].end},
},
Monday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Monday].start},
End: timeutil.Duration{Duration: w.days[time.Monday].end},
},
Tuesday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Tuesday].start},
End: timeutil.Duration{Duration: w.days[time.Tuesday].end},
},
Wednesday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Wednesday].start},
End: timeutil.Duration{Duration: w.days[time.Wednesday].end},
},
Thursday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Thursday].start},
End: timeutil.Duration{Duration: w.days[time.Thursday].end},
},
Friday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Friday].start},
End: timeutil.Duration{Duration: w.days[time.Friday].end},
},
Saturday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Saturday].start},
End: timeutil.Duration{Duration: w.days[time.Saturday].end},
},
}, nil
}
// dayRange represents a single interval within a day. The interval begins at
// start and ends before end. That is, it contains a time point T if start <=
// T < end.
type dayRange struct {
// start is an offset from the beginning of the day. It must be greater
// than or equal to zero and less than 24h.
start time.Duration
// end is an offset from the beginning of the day. It must be greater than
// or equal to zero and less than or equal to 24h.
end time.Duration
}
// validate returns the day range validation errors, if any.
func (r dayRange) validate() (err error) {
switch {
case r == dayRange{}:
return nil
case r.start < 0:
return fmt.Errorf("start %s is negative", r.start)
case r.end < 0:
return fmt.Errorf("end %s is negative", r.end)
case r.start >= r.end:
return fmt.Errorf("start %s is greater or equal to end %s", r.start, r.end)
case r.start >= maxDayRange:
return fmt.Errorf("start %s is greater or equal to %s", r.start, maxDayRange)
case r.end > maxDayRange:
return fmt.Errorf("end %s is greater than %s", r.end, maxDayRange)
default:
return nil
}
}
// contains returns true if start <= offset < end, where offset is the time
// duration from the beginning of the day.
func (r *dayRange) contains(offset time.Duration) (ok bool) {
return r.start <= offset && offset < r.end
}

View File

@@ -0,0 +1,371 @@
package schedule
import (
"testing"
"time"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestWeekly_Contains(t *testing.T) {
baseTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
otherTime := baseTime.Add(1 * timeutil.Day)
// NOTE: In the Etc area the sign of the offsets is flipped. So, Etc/GMT-3
// is actually UTC+03:00.
otherTZ := time.FixedZone("Etc/GMT-3", 3*60*60)
// baseSchedule, 12:00 to 14:00.
baseSchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 12 * time.Hour, end: 14 * time.Hour},
},
location: time.UTC,
}
// allDaySchedule, 00:00 to 24:00.
allDaySchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 0, end: 24 * time.Hour},
},
location: time.UTC,
}
// oneMinSchedule, 00:00 to 00:01.
oneMinSchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 0, end: 1 * time.Minute},
},
location: time.UTC,
}
testCases := []struct {
schedule *Weekly
assert assert.BoolAssertionFunc
t time.Time
name string
}{{
schedule: EmptyWeekly(),
assert: assert.False,
t: baseTime,
name: "empty",
}, {
schedule: allDaySchedule,
assert: assert.True,
t: baseTime,
name: "same_day_all_day",
}, {
schedule: baseSchedule,
assert: assert.True,
t: baseTime.Add(13 * time.Hour),
name: "same_day_inside",
}, {
schedule: baseSchedule,
assert: assert.False,
t: baseTime.Add(11 * time.Hour),
name: "same_day_outside",
}, {
schedule: allDaySchedule,
assert: assert.True,
t: baseTime.Add(24*time.Hour - time.Second),
name: "same_day_last_second",
}, {
schedule: allDaySchedule,
assert: assert.False,
t: otherTime,
name: "other_day_all_day",
}, {
schedule: baseSchedule,
assert: assert.False,
t: otherTime.Add(13 * time.Hour),
name: "other_day_inside",
}, {
schedule: baseSchedule,
assert: assert.False,
t: otherTime.Add(11 * time.Hour),
name: "other_day_outside",
}, {
schedule: baseSchedule,
assert: assert.True,
t: baseTime.Add(13 * time.Hour).In(otherTZ),
name: "same_day_inside_other_tz",
}, {
schedule: baseSchedule,
assert: assert.False,
t: baseTime.Add(11 * time.Hour).In(otherTZ),
name: "same_day_outside_other_tz",
}, {
schedule: oneMinSchedule,
assert: assert.True,
t: baseTime,
name: "one_minute_beginning",
}, {
schedule: oneMinSchedule,
assert: assert.True,
t: baseTime.Add(1*time.Minute - 1),
name: "one_minute_end",
}, {
schedule: oneMinSchedule,
assert: assert.False,
t: baseTime.Add(1 * time.Minute),
name: "one_minute_past_end",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.assert(t, tc.schedule.Contains(tc.t))
})
}
}
const brusselsSunday = `
sun:
start: 12h
end: 14h
time_zone: Europe/Brussels
`
func TestWeekly_UnmarshalYAML(t *testing.T) {
const (
sameTime = `
sun:
start: 9h
end: 9h
`
negativeStart = `
sun:
start: -1h
end: 1h
`
badTZ = `
time_zone: "bad_timezone"
`
badYAML = `
yaml: "bad"
yaml: "bad"
`
)
brusseltsTZ, err := time.LoadLocation("Europe/Brussels")
require.NoError(t, err)
brusselsWeekly := &Weekly{
days: [7]dayRange{{
start: time.Hour * 12,
end: time.Hour * 14,
}},
location: brusseltsTZ,
}
testCases := []struct {
name string
wantErrMsg string
data []byte
want *Weekly
}{{
name: "empty",
wantErrMsg: "",
data: []byte(""),
want: &Weekly{},
}, {
name: "null",
wantErrMsg: "",
data: []byte("null"),
want: &Weekly{},
}, {
name: "brussels_sunday",
wantErrMsg: "",
data: []byte(brusselsSunday),
want: brusselsWeekly,
}, {
name: "start_equal_end",
wantErrMsg: "weekday Sunday: bad day range: start 9h0m0s is greater or equal to end 9h0m0s",
data: []byte(sameTime),
want: &Weekly{},
}, {
name: "start_negative",
wantErrMsg: "weekday Sunday: bad day range: start -1h0m0s is negative",
data: []byte(negativeStart),
want: &Weekly{},
}, {
name: "bad_time_zone",
wantErrMsg: "unknown time zone bad_timezone",
data: []byte(badTZ),
want: &Weekly{},
}, {
name: "bad_yaml",
wantErrMsg: "yaml: unmarshal errors:\n line 3: mapping key \"yaml\" already defined at line 2",
data: []byte(badYAML),
want: &Weekly{},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w := &Weekly{}
err = yaml.Unmarshal(tc.data, w)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.want, w)
})
}
}
func TestWeekly_MarshalYAML(t *testing.T) {
brusselsTZ, err := time.LoadLocation("Europe/Brussels")
require.NoError(t, err)
brusselsWeekly := &Weekly{
days: [7]dayRange{time.Sunday: {
start: time.Hour * 12,
end: time.Hour * 14,
}},
location: brusselsTZ,
}
testCases := []struct {
name string
data []byte
want *Weekly
}{{
name: "empty",
data: []byte(""),
want: &Weekly{},
}, {
name: "null",
data: []byte("null"),
want: &Weekly{},
}, {
name: "brussels_sunday",
data: []byte(brusselsSunday),
want: brusselsWeekly,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var data []byte
data, err = yaml.Marshal(brusselsWeekly)
require.NoError(t, err)
w := &Weekly{}
err = yaml.Unmarshal(data, w)
require.NoError(t, err)
assert.Equal(t, brusselsWeekly, w)
})
}
}
func TestWeekly_Validate(t *testing.T) {
testCases := []struct {
name string
in dayRange
wantErrMsg string
}{{
name: "empty",
wantErrMsg: "",
in: dayRange{},
}, {
name: "start_seconds",
wantErrMsg: "bad day range: start 1s isn't rounded to minutes",
in: dayRange{
start: time.Second,
end: time.Hour,
},
}, {
name: "end_seconds",
wantErrMsg: "bad day range: end 1s isn't rounded to minutes",
in: dayRange{
start: 0,
end: time.Second,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w := &Weekly{}
err := w.validate(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}
func TestDayRange_Validate(t *testing.T) {
testCases := []struct {
name string
in dayRange
wantErrMsg string
}{{
name: "empty",
wantErrMsg: "",
in: dayRange{},
}, {
name: "valid",
wantErrMsg: "",
in: dayRange{
start: time.Hour,
end: time.Hour * 2,
},
}, {
name: "valid_end_max",
wantErrMsg: "",
in: dayRange{
start: 0,
end: time.Hour * 24,
},
}, {
name: "start_negative",
wantErrMsg: "start -1h0m0s is negative",
in: dayRange{
start: time.Hour * -1,
end: time.Hour * 2,
},
}, {
name: "end_negative",
wantErrMsg: "end -1h0m0s is negative",
in: dayRange{
start: 0,
end: time.Hour * -1,
},
}, {
name: "start_equal_end",
wantErrMsg: "start 1h0m0s is greater or equal to end 1h0m0s",
in: dayRange{
start: time.Hour,
end: time.Hour,
},
}, {
name: "start_greater_end",
wantErrMsg: "start 2h0m0s is greater or equal to end 1h0m0s",
in: dayRange{
start: time.Hour * 2,
end: time.Hour,
},
}, {
name: "start_equal_max",
wantErrMsg: "start 24h0m0s is greater or equal to 24h0m0s",
in: dayRange{
start: time.Hour * 24,
end: time.Hour * 48,
},
}, {
name: "end_greater_max",
wantErrMsg: "end 48h0m0s is greater than 24h0m0s",
in: dayRange{
start: 0,
end: time.Hour * 48,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.in.validate()
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View File

@@ -9,7 +9,8 @@ require (
github.com/kisielk/errcheck v1.6.3
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.16.0
golang.org/x/tools v0.9.3
github.com/uudashr/gocognit v1.0.6
golang.org/x/tools v0.10.0
golang.org/x/vuln v0.1.0
honnef.co/go/tools v0.4.3
mvdan.cc/gofumpt v0.5.0
@@ -26,8 +27,8 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -40,6 +40,8 @@ github.com/securego/gosec/v2 v2.16.0 h1:Pi0JKoasQQ3NnoRao/ww/N/XdynIB9NRYYZT5CyO
github.com/securego/gosec/v2 v2.16.0/go.mod h1:xvLcVZqUfo4aAQu56TNv7/Ltz6emAOQAEsrZrt7uGlI=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y=
github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -56,20 +58,21 @@ golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:AbB0pIl
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -79,8 +82,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -92,8 +96,9 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/vuln v0.1.0 h1:9GRdj6wAIkDrsMevuolY+SXERPjQPp2P1ysYA0jpZe0=
golang.org/x/vuln v0.1.0/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -9,6 +9,7 @@ import (
_ "github.com/kisielk/errcheck"
_ "github.com/kyoh86/looppointer"
_ "github.com/securego/gosec/v2/cmd/gosec"
_ "github.com/uudashr/gocognit/cmd/gocognit"
_ "golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness"
_ "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow"
_ "golang.org/x/vuln/cmd/govulncheck"

View File

@@ -143,14 +143,7 @@ func Verbose() (v string) {
runtime.Version(),
)
if committime != "" {
commitTimeUnix, err := strconv.ParseInt(committime, 10, 64)
if err != nil {
stringutil.WriteToBuilder(b, nl, vFmtTimeHdr, fmt.Sprintf("parse error: %s", err))
} else {
stringutil.WriteToBuilder(b, nl, vFmtTimeHdr, time.Unix(commitTimeUnix, 0).String())
}
}
writeCommitTime(b)
stringutil.WriteToBuilder(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr)
if goarm != "" {
@@ -179,3 +172,16 @@ func Verbose() (v string) {
return b.String()
}
func writeCommitTime(b *strings.Builder) {
if committime == "" {
return
}
commitTimeUnix, err := strconv.ParseInt(committime, 10, 64)
if err != nil {
stringutil.WriteToBuilder(b, "\n", vFmtTimeHdr, fmt.Sprintf("parse error: %s", err))
} else {
stringutil.WriteToBuilder(b, "\n", vFmtTimeHdr, time.Unix(commitTimeUnix, 0).String())
}
}

376
internal/whois/whois.go Normal file
View File

@@ -0,0 +1,376 @@
// Package whois provides WHOIS functionality.
package whois
import (
"bytes"
"context"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/bluele/gcache"
)
const (
// DefaultServer is the default WHOIS server.
DefaultServer = "whois.arin.net"
// DefaultPort is the default port for WHOIS requests.
DefaultPort = 43
)
// Interface provides WHOIS functionality.
type Interface interface {
// Process makes WHOIS request and returns WHOIS information or nil.
// changed indicates that Info was updated since last request.
Process(ctx context.Context, ip netip.Addr) (info *Info, changed bool)
}
// Empty is an empty [Interface] implementation which does nothing.
type Empty struct{}
// type check
var _ Interface = (*Empty)(nil)
// Process implements the [Interface] interface for Empty.
func (Empty) Process(_ context.Context, _ netip.Addr) (info *Info, changed bool) {
return nil, false
}
// Config is the configuration structure for Default.
type Config struct {
// DialContext specifies the dial function for creating unencrypted TCP
// connections.
DialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// ServerAddr is the address of the WHOIS server.
ServerAddr string
// Timeout is the timeout for WHOIS requests.
Timeout time.Duration
// CacheTTL is the Time to Live duration for cached IP addresses.
CacheTTL time.Duration
// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.
MaxConnReadSize int64
// MaxRedirects is the maximum redirects count.
MaxRedirects int
// MaxInfoLen is the maximum length of Info fields returned by Process.
MaxInfoLen int
// CacheSize is the maximum size of the cache. It must be greater than
// zero.
CacheSize int
// Port is the port for WHOIS requests.
Port uint16
}
// Default is the default WHOIS information processor.
type Default struct {
// cache is the cache containing IP addresses of clients. An active IP
// address is resolved once again after it expires. If IP address couldn't
// be resolved, it stays here for some time to prevent further attempts to
// resolve the same IP.
cache gcache.Cache
// dialContext connects to a remote server resolving hostname using our own
// DNS server and unecrypted TCP connection.
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// serverAddr is the address of the WHOIS server.
serverAddr string
// portStr is the port for WHOIS requests.
portStr string
// timeout is the timeout for WHOIS requests.
timeout time.Duration
// cacheTTL is the Time to Live duration for cached IP addresses.
cacheTTL time.Duration
// maxConnReadSize is an upper limit in bytes for reading from net.Conn.
maxConnReadSize int64
// maxRedirects is the maximum redirects count.
maxRedirects int
// maxInfoLen is the maximum length of Info fields returned by Process.
maxInfoLen int
}
// New returns a new default WHOIS information processor. conf must not be
// nil.
func New(conf *Config) (w *Default) {
return &Default{
serverAddr: conf.ServerAddr,
dialContext: conf.DialContext,
timeout: conf.Timeout,
cache: gcache.New(conf.CacheSize).LRU().Build(),
maxConnReadSize: conf.MaxConnReadSize,
maxRedirects: conf.MaxRedirects,
portStr: strconv.Itoa(int(conf.Port)),
maxInfoLen: conf.MaxInfoLen,
cacheTTL: conf.CacheTTL,
}
}
// trimValue trims s and replaces the last 3 characters of the cut with "..."
// to fit into max. max must be greater than 3.
func trimValue(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-3] + "..."
}
// isWHOISComment returns true if the data is empty or is a WHOIS comment.
func isWHOISComment(data []byte) (ok bool) {
return len(data) == 0 || data[0] == '#' || data[0] == '%'
}
// whoisParse parses a subset of plain-text data from the WHOIS response into a
// string map. It trims values of the returned map to maxLen.
func whoisParse(data []byte, maxLen int) (info map[string]string) {
info = map[string]string{}
var orgname string
lines := bytes.Split(data, []byte("\n"))
for _, l := range lines {
if isWHOISComment(l) {
continue
}
before, after, found := bytes.Cut(l, []byte(":"))
if !found {
continue
}
key := strings.ToLower(string(before))
val := strings.TrimSpace(string(after))
if val == "" {
continue
}
switch key {
case "orgname", "org-name":
key = "orgname"
val = trimValue(val, maxLen)
orgname = val
case "city", "country":
val = trimValue(val, maxLen)
case "descr", "netname":
key = "orgname"
val = stringutil.Coalesce(orgname, val)
orgname = val
case "whois":
key = "whois"
case "referralserver":
key = "whois"
val = strings.TrimPrefix(val, "whois://")
default:
continue
}
info[key] = val
}
return info
}
// query sends request to a server and returns the response or error.
func (w *Default) query(ctx context.Context, target, serverAddr string) (data []byte, err error) {
addr, _, _ := net.SplitHostPort(serverAddr)
if addr == DefaultServer {
// Display type flags for query.
//
// See https://www.arin.net/resources/registry/whois/rws/api/#nicname-whois-queries.
target = "n + " + target
}
conn, err := w.dialContext(ctx, "tcp", serverAddr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
r, err := aghio.LimitReader(conn, w.maxConnReadSize)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
_ = conn.SetReadDeadline(time.Now().Add(w.timeout))
_, err = io.WriteString(conn, target+"\r\n")
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
// This use of ReadAll is now safe, because we limited the conn Reader.
data, err = io.ReadAll(r)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
return data, nil
}
// queryAll queries WHOIS server and handles redirects.
func (w *Default) queryAll(ctx context.Context, target string) (info map[string]string, err error) {
server := net.JoinHostPort(w.serverAddr, w.portStr)
var data []byte
for i := 0; i < w.maxRedirects; i++ {
data, err = w.query(ctx, target, server)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
log.Debug("whois: received response (%d bytes) from %q about %q", len(data), server, target)
info = whoisParse(data, w.maxInfoLen)
redir, ok := info["whois"]
if !ok {
return info, nil
}
redir = strings.ToLower(redir)
_, _, err = net.SplitHostPort(redir)
if err != nil {
server = net.JoinHostPort(redir, w.portStr)
} else {
server = redir
}
log.Debug("whois: redirected to %q about %q", redir, target)
}
return nil, fmt.Errorf("whois: redirect loop")
}
// type check
var _ Interface = (*Default)(nil)
// Process makes WHOIS request and returns WHOIS information or nil. changed
// indicates that Info was updated since last request.
func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed bool) {
if netutil.IsSpecialPurposeAddr(ip) {
return nil, false
}
wi, expired := w.findInCache(ip)
if wi != nil && !expired {
// Don't return an empty struct so that the frontend doesn't get
// confused.
if (*wi == Info{}) {
return nil, false
}
return wi, false
}
var info Info
defer func() {
item := toCacheItem(info, w.cacheTTL)
err := w.cache.Set(ip, item)
if err != nil {
log.Debug("whois: cache: adding item %q: %s", ip, err)
}
}()
kv, err := w.queryAll(ctx, ip.String())
if err != nil {
log.Debug("whois: quering about %q: %s", ip, err)
return nil, true
}
info = Info{
City: kv["city"],
Country: kv["country"],
Orgname: kv["orgname"],
}
// Don't return an empty struct so that the frontend doesn't get confused.
if (info == Info{}) {
return nil, true
}
return &info, wi == nil || info != *wi
}
// findInCache finds Info in the cache. expired indicates that Info is valid.
func (w *Default) findInCache(ip netip.Addr) (wi *Info, expired bool) {
val, err := w.cache.Get(ip)
if err != nil {
if !errors.Is(err, gcache.KeyNotFoundError) {
log.Debug("whois: cache: retrieving info about %q: %s", ip, err)
}
return nil, false
}
item, ok := val.(*cacheItem)
if !ok {
log.Debug("whois: cache: %q bad type %T", ip, val)
return nil, false
}
return fromCacheItem(item)
}
// Info is the filtered WHOIS data for a runtime client.
type Info struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
}
// cacheItem represents an item that we will store in the cache.
type cacheItem struct {
// expiry is the time when cacheItem will expire.
expiry time.Time
// info is the WHOIS data for a runtime client.
info *Info
}
// toCacheItem creates a cached item from a WHOIS info and Time to Live
// duration.
func toCacheItem(info Info, ttl time.Duration) (item *cacheItem) {
return &cacheItem{
expiry: time.Now().Add(ttl),
info: &info,
}
}
// fromCacheItem creates a WHOIS info from the cached item. expired indicates
// that WHOIS info is valid. item must not be nil.
func fromCacheItem(item *cacheItem) (info *Info, expired bool) {
if time.Now().After(item.expiry) {
return item.info, true
}
return item.info, false
}

View File

@@ -0,0 +1,155 @@
package whois_test
import (
"context"
"io"
"net"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/testutil/fakenet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefault_Process(t *testing.T) {
const (
nl = "\n"
city = "Nonreal"
country = "Imagiland"
orgname = "FakeOrgLLC"
referralserver = "whois.example.net"
)
ip := netip.MustParseAddr("1.2.3.4")
testCases := []struct {
want *whois.Info
name string
data string
}{{
want: nil,
name: "empty",
data: "",
}, {
want: nil,
name: "comments",
data: "%\n#",
}, {
want: nil,
name: "no_colon",
data: "city",
}, {
want: nil,
name: "no_value",
data: "city:",
}, {
want: &whois.Info{
City: city,
},
name: "city",
data: "city: " + city,
}, {
want: &whois.Info{
Country: country,
},
name: "country",
data: "country: " + country,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname",
data: "orgname: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_hyphen",
data: "org-name: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_descr",
data: "descr: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_netname",
data: "netname: " + orgname,
}, {
want: &whois.Info{
City: city,
Country: country,
Orgname: orgname,
},
name: "full",
data: "OrgName: " + orgname + nl + "City: " + city + nl + "Country: " + country,
}, {
want: nil,
name: "whois",
data: "whois: " + referralserver,
}, {
want: nil,
name: "referralserver",
data: "referralserver: whois://" + referralserver,
}, {
want: nil,
name: "other",
data: "other: value",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hit := 0
fakeConn := &fakenet.Conn{
OnRead: func(b []byte) (n int, err error) {
hit++
return copy(b, tc.data), io.EOF
},
OnWrite: func(b []byte) (n int, err error) {
return len(b), nil
},
OnClose: func() (err error) {
return nil
},
OnSetReadDeadline: func(t time.Time) (err error) {
return nil
},
}
w := whois.New(&whois.Config{
Timeout: 5 * time.Second,
DialContext: func(_ context.Context, _, addr string) (_ net.Conn, _ error) {
hit = 0
return fakeConn, nil
},
MaxConnReadSize: 1024,
MaxRedirects: 3,
MaxInfoLen: 250,
CacheSize: 100,
CacheTTL: time.Hour,
})
got, changed := w.Process(context.Background(), ip)
require.True(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
// From cache.
got, changed = w.Process(context.Background(), ip)
require.False(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
})
}
}

View File

@@ -6,6 +6,17 @@
## v0.107.30: API changes
### New HTTP API 'GET /control/querylog/export'
* The new `GET /control/querylog/export` HTTP API allows an export of query log
items in the CSV file. It returns a CSV object with the following format:
```csv
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
```
### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type
* The value of the `Content-Type` header in the `POST /control/version.json` and

View File

@@ -313,6 +313,51 @@
'responses':
'200':
'description': 'OK.'
'/querylog/export':
'get':
'tags':
- 'log'
'description': >
Returns a CSV file stream with the following fields, sorted a-z:
ans_dnssec, ans_rcode, ans_type, ans_value, cached, client_ip,
clientid, ecs, elapsed, filter_id, filter_rule, proto, qclass, qname,
qtype, reason, time, upstream. The fields list is a subject to change.
The content is UTF-8 encoded with quotation marks.
'operationId': 'getQueryLogExport'
'summary': 'Get DNS server query log items in a CSV stream.'
'parameters':
- 'name': 'search'
'in': 'query'
'description': 'Filter by domain name or client IP'
'schema':
'type': 'string'
- 'name': 'response_status'
'in': 'query'
'description': 'Filter by response status'
'schema':
'type': 'string'
'enum':
- 'all'
- 'filtered'
- 'blocked'
- 'blocked_safebrowsing'
- 'blocked_parental'
- 'whitelisted'
- 'rewritten'
- 'safe_search'
- 'processed'
'responses':
'200':
'description': 'OK.'
'content':
'text/csv':
'schema':
'type': 'string'
'example': >
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
'/stats':
'get':
'tags':

View File

@@ -2,7 +2,7 @@
## `hooks/`: Git Hooks
### Usage
### Usage
Run `make init` from the project root.
@@ -10,7 +10,7 @@ Run `make init` from the project root.
## `querylog/`: Query Log Helpers
### Usage
### Usage
* `npm install`: install dependencies. Run this first.
* `npm run anonymize <source> <dst>`: read the query log from the `<source>`
@@ -26,157 +26,215 @@ don't print anything, and `1`, be verbose.
### `build-docker.sh`: Build A Multi-Architecture Docker Image
### `build-docker.sh`: Build A Multi-Architecture Docker Image
Required environment:
* `CHANNEL`: release channel, see above.
* `COMMIT`: current Git revision.
* `DIST_DIR`: the directory where a release has previously been built.
* `VERSION`: release version.
Optional environment:
* `DOCKER_IMAGE_NAME`: the name of the resulting Docker container. By default
it's `adguardhome-dev`.
* `DOCKER_OUTPUT`: the `--output` parameters. By default they are
`type=image,name=${DOCKER_IMAGE_NAME},push=false`.
* `SUDO`: allow users to use `sudo` or `doas` with `docker`. By default none
is used.
### `build-release.sh`: Build A Release For All Platforms
### `build-release.sh`: Build A Release For All Platforms
Required environment:
* `CHANNEL`: release channel, see above.
* `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN`
is `1`.
Optional environment:
* `ARCH` and `OS`: space-separated list of architectures and operating systems
for which to build a release. For example, to build only for 64-bit ARM and
AMD on Linux and Darwin:
```sh
make ARCH='amd64 arm64' OS='darwin linux' … build-release
```
The default value is `''`, which means build everything.
* `BUILD_SNAP`: `0` to not build Snapcraft packages, `1` to build. The
default value is `1`.
* `DIST_DIR`: the directory to build a release into. The default value is
`dist`.
* `GO`: set an alternative name for the Go compiler.
* `SIGN`: `0` to not sign the resulting packages, `1` to sign. The default
value is `1`.
* `VERBOSE`: `1` to be verbose, `2` to also print environment. This script
calls `go-build.sh` with the verbosity level one level lower, so to get
verbosity level `2` in `go-build.sh`, set this to `3` when calling
`build-release.sh`.
* `VERSION`: release version. Will be set by `version.sh` if it is unset or
if it has the default `Makefile` value of `v0.0.0`.
### `clean.sh`: Cleanup
### `clean.sh`: Cleanup
Optional environment:
* `GO`: set an alternative name for the Go compiler.
Required environment:
* `DIST_DIR`: the directory where a release has previously been built.
### `go-build.sh`: Build The Backend
### `go-build.sh`: Build The Backend
Optional environment:
* `GOARM`: ARM processor options for the Go compiler.
* `GOMIPS`: ARM processor options for the Go compiler.
* `GO`: set an alternative name for the Go compiler.
* `OUT`: output binary name.
* `PARALLELISM`: set the maximum number of concurrently run build commands
(that is, compiler, linker, etc.).
* `SOURCE_DATE_EPOCH`: the [standardized][repr] environment variable for the
Unix epoch time of the latest commit in the repository. If set, overrides
the default obtained from Git. Useful for reproducible builds.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands and environment.
The default value is `0`, don't be verbose.
* `VERSION`: release version. Will be set by `version.sh` if it is unset or
if it has the default `Makefile` value of `v0.0.0`.
Required environment:
* `CHANNEL`: release channel, see above.
[repr]: https://reproducible-builds.org/docs/source-date-epoch/
### `go-deps.sh`: Install Backend Dependencies
### `go-deps.sh`: Install Backend Dependencies
Optional environment:
* `GO`: set an alternative name for the Go compiler.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands and environment.
The default value is `0`, don't be verbose.
### `go-lint.sh`: Run Backend Static Analyzers
### `go-lint.sh`: Run Backend Static Analyzers
Don't forget to run `make go-tools` once first!
Optional environment:
* `EXIT_ON_ERROR`: if set to `0`, don't exit the script after the first
encountered error. The default value is `1`.
* `GO`: set an alternative name for the Go compiler.
* `VERBOSE`: verbosity level. `1` shows every command that is run. `2` also
shows subcommands. The default value is `0`, don't be verbose.
### `go-test.sh`: Run Backend Tests
### `go-test.sh`: Run Backend Tests
Optional environment:
* `GO`: set an alternative name for the Go compiler.
* `RACE`: set to `0` to not use the Go race detector. The default value is
`1`, use the race detector.
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
`--timeout 30s`.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands. The default
value is `0`, don't be verbose.
### `go-tools.sh`: Install Backend Tooling
### `go-tools.sh`: Install Backend Tooling
Installs the Go static analysis and other tools into `${PWD}/bin`. Either add
`${PWD}/bin` to your `$PATH` before all other entries, or use the commands
directly, or use the commands through `make` (for example, `make go-lint`).
Optional environment:
* `GO`: set an alternative name for the Go compiler.
### `version.sh`: Generate And Print The Current Version
### `version.sh`: Generate And Print The Current Version
Required environment:
* `CHANNEL`: release channel, see above.
## `snap/`: Snap GUI Files
## `snap/`: Snapcraft scripts
App icons (see https://github.com/AdguardTeam/AdGuardHome/pull/1836), Snap
manifest file templates, and helper scripts.
### `build.sh`
Builds the Snapcraft packages from the binaries created by `download.sh`.
### `download.sh`
Downloads the binaries to pack them into Snapcraft packages.
Required environment:
* `CHANNEL`: release channel, see above.
### `upload.sh`
Uploads the Snapcraft packages created by `build.sh`.
Required environment:
* `SNAPCRAFT_CHANNEL`: Snapcraft release channel: `edge`, `beta`, or
`candidate`.
* `SNAPCRAFT_STORE_CREDENTIALS`: Credentials for Snapcraft store.
Optional environment:
* `SNAPCRAFT_CMD`: Overrides the Snapcraft command. Default: `snapcraft`.
## `translations/`: Twosky Integration Script
### Usage
### Usage
* `go run main.go help`: print usage.
@@ -211,7 +269,7 @@ Optional environment:
A simple script that downloads and updates the companies DB in the `client`
code from [the repo][companiesrepo].
### Usage
### Usage
```sh
sh ./scripts/companiesdb/download.sh
@@ -231,7 +289,7 @@ Optional environment:
* `URL`: the URL of the index file. By default it's
`https://adguardteam.github.io/HostlistsRegistry/assets/services.json`.
### Usage
### Usage
```sh
go run ./scripts/blocked-services/main.go
@@ -251,7 +309,7 @@ Optional environment:
* `URL`: the URL of the index file. By default it's
`https://adguardteam.github.io/HostlistsRegistry/assets/filters.json`.
### Usage
### Usage
```sh
go run ./scripts/vetted-filters/main.go

View File

@@ -5,11 +5,12 @@ verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
debug_flags='-D'
debug_flags='--debug=1'
else
set +x
debug_flags=''
debug_flags='--debug=0'
fi
readonly debug_flags
set -e -f -u
@@ -61,21 +62,16 @@ readonly docker_output
case "$channel"
in
('release')
docker_image_full_name="${docker_image_name}:${version}"
docker_tags="--tag ${docker_image_name}:latest"
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:latest"
;;
('beta')
docker_image_full_name="${docker_image_name}:${version}"
docker_tags="--tag ${docker_image_name}:beta"
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:beta"
;;
('edge')
# Don't set the version tag when pushing to the edge channel.
docker_image_full_name="${docker_image_name}:edge"
docker_tags=''
docker_tags="--tag=${docker_image_name}:edge"
;;
('development')
docker_image_full_name="${docker_image_name}"
docker_tags=''
docker_tags="--tag=${docker_image_name}"
;;
(*)
echo "invalid channel '$channel', supported values are\
@@ -83,7 +79,7 @@ in
exit 1
;;
esac
readonly docker_image_full_name docker_tags
readonly docker_tags
# Copy the binaries into a new directory under new names, so that it's easier to
# COPY them later. DO NOT remove the trailing underscores. See file
@@ -117,10 +113,8 @@ cp "./docker/web-bind.awk"\
cp "./docker/healthcheck.sh"\
"${dist_docker_scripts}/healthcheck.sh"
# Don't use quotes with $docker_tags and $debug_flags because we want word
# splitting and or an empty space if tags are empty.
$sudo_cmd docker\
$debug_flags\
"$debug_flags"\
buildx build\
--build-arg BUILD_DATE="$build_date"\
--build-arg DIST_DIR="$dist_dir"\
@@ -128,7 +122,6 @@ $sudo_cmd docker\
--build-arg VERSION="$version"\
--output "$docker_output"\
--platform "$docker_platforms"\
$docker_tags\
-t "$docker_image_full_name"\
"$docker_tags"\
-f ./docker/Dockerfile\
.

View File

@@ -78,14 +78,6 @@ else
fi
readonly oses
snap_enabled="${BUILD_SNAP:-1}"
readonly snap_enabled
if [ "$snap_enabled" -eq '0' ]
then
log 'snap: disabled'
fi
# Require the gpg key and passphrase to be set if the signing is required.
if [ "$sign" -eq '1' ]
then
@@ -106,7 +98,7 @@ log "checking tools"
# Make sure we fail gracefully if one of the tools we need is missing. Use
# alternatives when available.
use_shasum='0'
for tool in gpg gzip sed sha256sum snapcraft tar zip
for tool in gpg gzip sed sha256sum tar zip
do
if ! command -v "$tool" > /dev/null
then
@@ -128,36 +120,36 @@ readonly use_shasum
# Data section. Arrange data into space-separated tables for read -r to read.
# Use a hyphen for missing values.
# os arch arm mips snap
# os arch arm mips
platforms="\
darwin amd64 - - -
darwin arm64 - - -
freebsd 386 - - -
freebsd amd64 - - -
freebsd arm 5 - -
freebsd arm 6 - -
freebsd arm 7 - -
freebsd arm64 - - -
linux 386 - - i386
linux amd64 - - amd64
linux arm 5 - -
linux arm 6 - -
linux arm 7 - armhf
linux arm64 - - arm64
linux mips - softfloat -
linux mips64 - softfloat -
linux mips64le - softfloat -
linux mipsle - softfloat -
linux ppc64le - - -
openbsd amd64 - - -
openbsd arm64 - - -
windows 386 - - -
windows amd64 - - -
windows arm64 - - -"
darwin amd64 - -
darwin arm64 - -
freebsd 386 - -
freebsd amd64 - -
freebsd arm 5 -
freebsd arm 6 -
freebsd arm 7 -
freebsd arm64 - -
linux 386 - -
linux amd64 - -
linux arm 5 -
linux arm 6 -
linux arm 7 -
linux arm64 - -
linux mips - softfloat
linux mips64 - softfloat
linux mips64le - softfloat
linux mipsle - softfloat
linux ppc64le - -
openbsd amd64 - -
openbsd arm64 - -
windows 386 - -
windows amd64 - -
windows arm64 - -"
readonly platforms
# Function build builds the release for one platform. It builds a binary, an
# archive and, if needed, a snap package.
# Function build builds the release for one platform. It builds a binary and an
# archive.
build() {
# Get the arguments. Here and below, use the "build_" prefix for all
# variables local to function build.
@@ -167,7 +159,6 @@ build() {
build_arch="$4"\
build_arm="$5"\
build_mips="$6"\
build_snap="$7"\
;
# Use the ".exe" filename extension if we build a Windows release.
@@ -229,52 +220,13 @@ build() {
esac
log "$build_archive"
# Exit if we don't need to build the Snap package.
if [ "$build_snap" = '-' ] || [ "$snap_enabled" -eq '0' ]
then
return
fi
# Prepare the Snap build.
build_snap_output="./${dist}/AdGuardHome_${build_snap}.snap"
build_snap_dir="${build_snap_output}.dir"
# Create the meta subdirectory and copy files there.
mkdir -p "${build_snap_dir}/meta"
cp "$build_output" './scripts/snap/local/adguard-home-web.sh' "$build_snap_dir"
cp -r './scripts/snap/gui' "${build_snap_dir}/meta/"
# Create a snap.yaml file, setting the values.
sed -e 's/%VERSION%/'"$version"'/'\
-e 's/%ARCH%/'"$build_snap"'/'\
./scripts/snap/snap.tmpl.yaml\
>"${build_snap_dir}/meta/snap.yaml"
# TODO(a.garipov): The snapcraft tool will *always* write everything,
# including errors, to stdout. And there doesn't seem to be a way to change
# that. So, save the combined output, but only show it when snapcraft
# actually fails.
set +e
build_snapcraft_output="$(
snapcraft pack "$build_snap_dir" --output "$build_snap_output" 2>&1
)"
build_snapcraft_exit_code="$?"
set -e
if [ "$build_snapcraft_exit_code" -ne '0' ]
then
log "$build_snapcraft_output"
exit "$build_snapcraft_exit_code"
fi
log "$build_snap_output"
}
log "starting builds"
# Go over all platforms defined in the space-separated table above, tweak the
# values where necessary, and feed to build.
echo "$platforms" | while read -r os arch arm mips snap
echo "$platforms" | while read -r os arch arm mips
do
# See if the architecture or the OS is in the allowlist. To do so, try
# removing everything that matches the pattern (well, a prefix, but that
@@ -314,7 +266,7 @@ do
;;
esac
build "$dir" "$ar" "$os" "$arch" "$arm" "$mips" "$snap"
build "$dir" "$ar" "$os" "$arch" "$arm" "$mips"
done
log "packing frontend"
@@ -413,14 +365,14 @@ do
platform="$f"
# Remove the prefix.
platform="${platform#./${dist}/AdGuardHome_}"
platform="${platform#"./${dist}/AdGuardHome_"}"
# Remove the filename extensions.
platform="${platform%.zip}"
platform="${platform%.tar.gz}"
# Use the filename's base path.
filename="${f#./${dist}/}"
filename="${f#"./${dist}/"}"
if [ "$i" -eq "$ar_files_len" ]
then

View File

@@ -170,6 +170,17 @@ run_linter govulncheck ./...
run_linter gocyclo --over 10 .
# TODO(a.garipov): Enable for all.
run_linter gocognit --over 10\
./internal/aghalg/\
./internal/aghchan/\
./internal/aghhttp/\
./internal/aghio/\
./internal/tools/\
./internal/next/\
./internal/version/\
;
run_linter ineffassign ./...
run_linter unparam ./...

View File

@@ -38,6 +38,7 @@ readonly go
rm -f\
bin/errcheck\
bin/fieldalignment\
bin/gocognit\
bin/gocyclo\
bin/gofumpt\
bin/gosec\
@@ -69,6 +70,7 @@ env\
github.com/kisielk/errcheck\
github.com/kyoh86/looppointer/cmd/looppointer\
github.com/securego/gosec/v2/cmd/gosec\
github.com/uudashr/gocognit/cmd/gocognit\
golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment\
golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\
golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\

77
scripts/snap/build.sh Normal file
View File

@@ -0,0 +1,77 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller requested
# verbosity level greater than 0. Otherwise, it does nothing.
#
# TODO(a.garipov): Add to helpers.sh and use more actively in scripts.
log() {
if [ "$verbose" -gt '0' ]
then
# Don't use quotes to get word splitting.
echo "$1" 1>&2
fi
}
version="$( ./AdGuardHome_amd64 --version | cut -d ' ' -f 4 )"
if [ "$version" = '' ]
then
log 'empty version from ./AdGuardHome_amd64'
exit 1
fi
readonly version
log "version '$version'"
for arch in\
'i386'\
'amd64'\
'armhf'\
'arm64'
do
build_output="./AdGuardHome_${arch}"
snap_output="./AdGuardHome_${arch}.snap"
snap_dir="${snap_output}.dir"
# Create the meta subdirectory and copy files there.
mkdir -p "${snap_dir}/meta"
cp "$build_output" "${snap_dir}/AdGuardHome"
cp './snap/local/adguard-home-web.sh' "$snap_dir"
cp -r './snap/gui' "${snap_dir}/meta/"
# Create a snap.yaml file, setting the values.
sed\
-e 's/%VERSION%/'"$version"'/'\
-e 's/%ARCH%/'"$arch"'/'\
./snap/snap.tmpl.yaml\
> "${snap_dir}/meta/snap.yaml"
# TODO(a.garipov): The snapcraft tool will *always* write everything,
# including errors, to stdout. And there doesn't seem to be a way to change
# that. So, save the combined output, but only show it when snapcraft
# actually fails.
set +e
snapcraft_output="$(
snapcraft pack "$snap_dir" --output "$snap_output" 2>&1
)"
snapcraft_exit_code="$?"
set -e
if [ "$snapcraft_exit_code" -ne '0' ]
then
log "$snapcraft_output"
exit "$snapcraft_exit_code"
fi
log "$snap_output"
rm -f -r "$snap_dir"
done

29
scripts/snap/download.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
channel="${CHANNEL:?please set CHANNEL}"
readonly channel
printf '%s %s\n'\
'386' 'i386'\
'amd64' 'amd64'\
'armv7' 'armhf'\
'arm64' 'arm64' \
| while read -r arch snap_arch
do
release_url="https://static.adtidy.org/adguardhome/${channel}/AdGuardHome_linux_${arch}.tar.gz"
output="./AdGuardHome_linux_${arch}.tar.gz"
curl -o "$output" -v "$release_url"
tar -f "$output" -v -x -z
cp ./AdGuardHome/AdGuardHome "./AdGuardHome_${snap_arch}"
rm -f -r "$output" ./AdGuardHome
done

93
scripts/snap/upload.sh Normal file
View File

@@ -0,0 +1,93 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller requested
# verbosity level greater than 0. Otherwise, it does nothing.
log() {
if [ "$verbose" -gt '0' ]
then
# Don't use quotes to get word splitting.
echo "$1" 1>&2
fi
}
# Do not set a new lowercase variable, because the snapcraft tool expects the
# uppercase form.
if [ "${SNAPCRAFT_STORE_CREDENTIALS:-}" = '' ]
then
log 'please set SNAPCRAFT_STORE_CREDENTIALS'
exit 1
fi
export SNAPCRAFT_STORE_CREDENTIALS
snapcraft_channel="${SNAPCRAFT_CHANNEL:?please set SNAPCRAFT_CHANNEL}"
readonly snapcraft_channel
# Allow developers to overwrite the command, e.g. for testing.
snapcraft_cmd="${SNAPCRAFT_CMD:-snapcraft}"
readonly snapcraft_cmd
default_timeout='90s'
kill_timeout='120s'
readonly default_timeout kill_timeout
for arch in\
'i386'\
'amd64'\
'armhf'\
'arm64'
do
snap_file="./AdGuardHome_${arch}.snap"
# Catch the exit code and the combined output to later inspect it.
set +e
snapcraft_output="$(
# Use timeout(1) to force snapcraft to quit after a certain time. There
# seems to be no environment variable or flag to force this behavior.
timeout\
--preserve-status\
-k "$kill_timeout"\
-v "$default_timeout"\
"$snapcraft_cmd" upload\
--release="${snapcraft_channel}"\
--quiet\
"${snap_file}"\
2>&1
)"
snapcraft_exit_code="$?"
set -e
if [ "$snapcraft_exit_code" -eq '0' ]
then
log "successful upload: ${snapcraft_output}"
continue
fi
# Skip the ones that were failed by a duplicate upload error.
case "$snapcraft_output"
in
(*'A file with this exact same content has already been uploaded'|\
'Error checking upload uniqueness'*)
log "warning: duplicate upload, skipping"
log "snapcraft upload error: ${snapcraft_output}"
continue
;;
(*)
echo "unexpected snapcraft upload error: ${snapcraft_output}"
return "$snapcraft_exit_code"
;;
esac
done

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -4,13 +4,20 @@ initialisms = [
#
# Do not add "PTR" since we use "Ptr" as a suffix.
"inherit"
, "ASN"
, "DHCP"
, "DNSSEC"
# E.g. SentryDSN.
, "DSN"
, "ECS"
, "EDNS"
, "MX"
, "QUIC"
, "RA"
, "RRSIG"
, "SDNS"
, "SLAAC"
, "SOA"
, "SVCB"
, "TLD"
, "WHOIS"