Compare commits

...

22 Commits

Author SHA1 Message Date
Ainar Garipov
0a1887a854 Pull request 1914: upd-flts
Squashed commit of the following:

commit a8932f56fad583ecfcb7efae36fc516454bc6610
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 11 16:20:55 2023 +0300

    filtering: fix docs; upd svcs
2023-07-11 16:27:20 +03:00
Dimitry Kolyshev
65b526b969 Pull request: 5972-ip-dupl-ans
Updates #5972.

Squashed commit of the following:

commit 0e089f9ff8fd7e6d7cb53aa7c3b92435d1d41a81
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 11 15:33:16 2023 +0300

    dnsforward: imp code

commit 39527c078fd9ad6ea4906659e185d54e74ef6465
Merge: 03641b0b5 61ed74374
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 11 11:29:19 2023 +0300

    Merge remote-tracking branch 'origin/master' into 5972-ip-dupl-ans

    # Conflicts:
    #	CHANGELOG.md

commit 03641b0b511f8e48d386be76d0a4776296cf047d
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jul 10 14:03:28 2023 +0300

    all: dupl ips in answer
2023-07-11 15:46:01 +03:00
Ainar Garipov
61ed743748 Pull request 1913: parental-cache-size
Squashed commit of the following:

commit 6e7dcf0c59c478869e65cb6945d8d262b9eb1879
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 10 19:44:49 2023 +0300

    home: fix parental cache size
2023-07-10 20:00:29 +03:00
Ainar Garipov
c02a14117d Pull request 1912: 5896-safe-browsing-ptr
Updates #5896.

Squashed commit of the following:

commit 49340544a2a8762283397cdb54b91ed534591fa0
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 10 17:59:45 2023 +0300

    hashprefix: fix loop pointer
2023-07-10 19:04:31 +03:00
Ainar Garipov
7b92d53b84 Pull request 1910: new-rulelist-parser
Squashed commit of the following:

commit bac0da6818388d67840b8fe9b633ce0804964ed9
Merge: cb6759b63 f7dd83251
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jul 7 18:22:40 2023 +0300

    Merge branch 'master' into new-rulelist-parser

commit cb6759b63546b35074ec0ae04769ddb5e83ebac1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jul 7 12:18:44 2023 +0300

    all: upd tools

commit d28bf4cb42057b84e0c1325389db121a91f7c989
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jul 6 19:35:48 2023 +0300

    all: upd chlog

commit 7df637b00331dff5810c3a76f4a7d2cee24148f1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jul 6 19:30:42 2023 +0300

    rulelist: fix tabs

commit 0598d0d43504b246570e9ee76d79dff0d86413c5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jul 6 19:06:18 2023 +0300

    all: add go-bench, go-fuzz; imp docs

commit a9ab7726048e216b13876a85991f3e3e8696a029
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jul 6 18:18:14 2023 +0300

    filtering: add new parser
2023-07-07 18:27:33 +03:00
Ainar Garipov
f7dd832517 Pull request 1909: 5939-rm-healthcheck
Updates #5939.

Squashed commit of the following:

commit 087309b4ef100e97339f49cf1c2e90ba2fa4293f
Merge: 360df813d c21f958ea
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jul 7 13:18:52 2023 +0300

    Merge branch 'master' into 5939-rm-healthcheck

commit 360df813d995f935c591aaea9c56fe4372ca2281
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jul 5 14:16:18 2023 +0300

    all: rm docker healthcheck
2023-07-07 13:58:15 +03:00
Stanislav Chzhen
c21f958eaf Pull request 1878: AG-22597-imp-rdns
Squashed commit of the following:

commit ccad155c34989943d88a0a260c50845d1f4ece6b
Merge: 0cd889f6a 5a195b441
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 6 17:00:58 2023 +0300

    Merge branch 'master' into AG-22597-imp-rdns

commit 0cd889f6a500f5616af0f8d8fdcde0403b87ad4f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 6 12:20:49 2023 +0300

    dnsforward: imp code

commit 1aaa1998b914b0d53142c21fa3bdcae502e4f3f6
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jul 4 20:11:55 2023 +0300

    home: add todo

commit aed232fcf70ef546f373d5235b73abcb4fbb4b6c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jul 4 13:25:28 2023 +0300

    all: imp code, tests

commit 5c028c2766ffb8ebdc358a245a249c6a55d9ad81
Merge: 83d6ae7f6 97af062f7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jul 3 18:54:42 2023 +0300

    Merge branch 'master' into AG-22597-imp-rdns

commit 83d6ae7f61a7b81a8d73cd6d747035278c64fb70
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jul 3 18:53:05 2023 +0300

    home: imp code

commit 8153988dece0406e51a90a43eaffae59dba30a36
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 30 18:06:09 2023 +0300

    all: imp code

commit 00d3cc11a9378318f176aae00ddf972f255d575c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 30 13:05:04 2023 +0300

    all: add tests

commit ffdc95f237bfdb780922b4390d82cdc0154b0621
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 15:20:00 2023 +0300

    all: imp code, docs

commit 0dc60e2b355750ca701558927d22fb9ad187ea7e
Merge: 69dd56bdb d4a4bda64
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 15:13:19 2023 +0300

    Merge branch 'master' into AG-22597-imp-rdns

commit 69dd56bdb75056b0fa6bcf6538af7fff93383323
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 23 14:36:29 2023 +0300

    rdns: add tests

commit 16909b51adbe3a3f230291834cc9486dd8a0e8f8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Jun 19 16:28:26 2023 +0300

    rdns: extract rdns
2023-07-06 17:10:06 +03:00
Dimitry Kolyshev
5a195b441c Pull request: log-yaml-conf
Updates #4897.

Squashed commit of the following:

commit 8a961157c9930bf4859ce2209e5016ce94987e12
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jul 5 10:13:24 2023 +0400

    home: imp code

commit 509c07eed06311d773bc3e34b3ca28d9f14186fe
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 4 17:33:31 2023 +0400

    all: fix

commit f032e28f98552f238721491c998fca2d7d4b9802
Merge: ee0113435 c46516475
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 4 17:31:29 2023 +0400

    Merge remote-tracking branch 'origin/master' into log-yaml-conf

    # Conflicts:
    #	CHANGELOG.md

commit ee011343512e82d4e21cb402759d0284523ba02a
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 4 12:31:42 2023 +0400

    all: changelog

commit 07f4c4a244b1b6200d3056cde5ebced6254084a7
Merge: 2042c0753 97af062f7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jul 4 12:25:21 2023 +0400

    Merge remote-tracking branch 'origin/master' into log-yaml-conf

commit 2042c0753ec29de6045c3f1de6d075cb93d6ec27
Merge: a1d3a5130 8004b135b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jul 3 16:25:26 2023 +0400

    Merge remote-tracking branch 'origin/master' into log-yaml-conf

commit a1d3a51307e80f9e509bd6f3bee1a7b17bf1ffe6
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Jun 30 11:11:32 2023 +0400

    home: imp code

commit 2392a3b02620b8c38e88afb4d75988be85fe1338
Merge: 4224fbed7 39f5c50ac
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 16:46:47 2023 +0400

    home: imp code

commit 4224fbed7113e94bee44d0ab0272e8302a8086f3
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 16:39:35 2023 +0400

    home: imp code

commit 5ce708cc50bed83e64f062e599fc8b6143d0d44d
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 12:48:42 2023 +0400

    all: docs

commit 4b6d898a888818410f59b843c3ca1a685aafec82
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 12:20:54 2023 +0400

    home: imp code

commit 431b44eda71f488c747c3efa4da0a6c222b1cf06
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 12:03:17 2023 +0400

    home: imp tests

commit 53a5c31060018c37953beb27d80c46f92bbe14af
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 17:40:49 2023 +0400

    home: imp docs

commit bfa57d9f21e3326baafd3a52e91d54396d8e03fa
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 15:49:40 2023 +0400

    home: log conf

commit 49e06dca9dc2ceb2647b7e36dac145ccf78a6c3f
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 15:41:02 2023 +0400

    home: log conf

commit 9be432dea7cec8ae0c0d3ee1f73c58c76376d07e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 10:45:01 2023 +0400

    home: log conf
2023-07-06 10:09:04 +03:00
Ainar Garipov
c46516475d Pull request 1906: 5896-safe-browsing-cpu-ram
Updates #5896.

Squashed commit of the following:

commit 81ac59e2f95ef3ad6ac5c4668c07c35d69570454
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 4 16:07:33 2023 +0300

    hashprefix: fix cache coding
2023-07-04 16:17:18 +03:00
Ainar Garipov
97af062f7b Pull request 1905: upd-chlog
Squashed commit of the following:

commit 783d9c9265be564e91e62b24d0e0e3213f1f7d7d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 3 17:14:17 2023 +0300

    all: upd chlog
2023-07-03 17:17:29 +03:00
Ainar Garipov
8004b135b1 Pull request 1904: 5959-fix-error-days
Updates #5959.

* commit '4b9264531be50e81fe610050a12827b71bc3a9cd':
  clients: use constant a day in milliseconds
  clients: fix lint
  fix error days
2023-07-03 14:34:19 +03:00
Ildar Kamalov
4b9264531b clients: use constant a day in milliseconds 2023-07-03 14:29:33 +03:00
Ainar Garipov
9a506d3755 clients: fix lint 2023-07-03 14:23:18 +03:00
qingbo
e320eb29c2 fix error days 2023-07-03 18:48:14 +08:00
Ainar Garipov
282f11a7c2 Pull request 1903: upd-all
Squashed commit of the following:

commit 61838cb3e08dcfd16c9fa521a8243207ec2091aa
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 3 13:30:37 2023 +0300

    all: upd i18n, svcs, tools, trackers
2023-07-03 13:40:30 +03:00
Eugene Burkov
91f3e29c08 Pull request 1891: 5902-bootstrap-hosts
Merge in DNS/adguard-home from 5902-bootstrap-hosts to master

Updates #5902.

Squashed commit of the following:

commit fcc65d3a8d7566acc361f54b18d1af85045225e2
Merge: 0c336af07 1fd6cf1a2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Jun 30 12:29:06 2023 +0300

    Merge branch 'master' into 5902-bootstrap-hosts

commit 0c336af07d2864533e1f10029b4321d7cd210a47
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Jun 29 15:40:28 2023 +0300

    all: imp & simplify

commit 45aae90035b98b30199cc7fc92991528f4e968c0
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 20:24:43 2023 +0300

    all: imp code, docs

commit e3dbb5bfe5dfbde7af00f39adcc15e9711e5feb0
Merge: a33a8e93c 2069eddf9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 18:27:36 2023 +0300

    Merge branch 'master' into 5902-bootstrap-hosts

commit a33a8e93cb36f7d0c4472e524e44de6ff0ab6653
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 13:27:11 2023 +0300

    aghos: add type check

commit 781a3a248871df2ea37a936c8d6b0b11e2d2f3a4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 13:09:37 2023 +0300

    all: log changes

commit 4575368655356f84992fad2bfb78cbc1c88da25a
Merge: 636c440fc cf7c12c97
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 13:08:11 2023 +0300

    Merge branch 'master' into 5902-bootstrap-hosts

commit 636c440fca9cbdfd5c12b7f89432fb9323e01d86
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jun 28 13:06:32 2023 +0300

    all: imp tests

commit 0eff7a747e32216d78abf9db9460cb9d48f31f96
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Jun 26 18:40:22 2023 +0300

    dnsforward: imp code

commit 7489a30971e3c76b8f62fd4ca11a977eeabe2cf5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Jun 26 17:04:10 2023 +0300

    all: resolve upstreams with hosts
2023-06-30 12:41:10 +03:00
Stanislav Chzhen
1fd6cf1a2f Pull request 1901: 5946-fix-blocked-services-client-schedule
Updates #5946.

Squashed commit of the following:

commit cd6ba613fae56d05a2e51ae1a65e9fcf4a39899e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jun 30 11:27:52 2023 +0300

    home: fix blocked services client schedule
2023-06-30 11:56:03 +03:00
Ainar Garipov
efed23701a Pull request 1900: fix-docker-script
Closes #5947.

Squashed commit of the following:

commit 309fab7afd78585a561830379f06dd531e6f260e
Merge: 31a6a577d ad1bf5cf6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 30 10:43:36 2023 +0300

    Merge branch 'master' into fix-docker-script

commit 31a6a577ddd2ae8f6a27f852f655628333f8e3da
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 29 21:48:37 2023 +0300

    scripts: fix unbound variable

commit 59b2bb836a11b2f2719d5c5a9bf5d025736d4435
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 29 21:12:37 2023 +0300

    scripts: fix docker
2023-06-30 10:47:44 +03:00
Stanislav Chzhen
ad1bf5cf6e Pull request 1898: fix-whois-gocognit
Squashed commit of the following:

commit 69675b742a3b8a1d513f1b5f9c488577ced329b8
Merge: 0d069b769 ee8eb1d8a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 19:31:26 2023 +0300

    Merge branch 'master' into fix-whois-gocognit

commit 0d069b76938d9b3011b8d92865e7630027f1ebd1
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 19:21:02 2023 +0300

    whois: imp code more

commit 6c4ab4a4e0451551bcec5472d997a88989f013e4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 18:08:50 2023 +0300

    whois: imp code

commit 0d04ddbd6f8bb3848673c3f86fc26dca2fd4d402
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 17:47:06 2023 +0300

    whois: imp docs

commit d7b75f7b42604374cb8cb0aaf141eb5c7fc985d3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jun 29 16:54:49 2023 +0300

    all: imp code
2023-06-29 19:35:33 +03:00
Ainar Garipov
ee8eb1d8a6 Pull request 1899: nextapi-pidfile-webaddr
Squashed commit of the following:

commit 73b97b638016dd3992376c2cd7d11b2e85b2c3a4
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 29 18:43:05 2023 +0300

    next: use maybe; sync conf

commit 99e18b8fbfad11343a1e66f746085d54be7aafea
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Jun 29 18:13:13 2023 +0300

    next: add local frontend, pidfile, webaddr
2023-06-29 19:10:39 +03:00
Dimitry Kolyshev
39f5c50acd Pull request: home: http conf
Updates #2860.

Squashed commit of the following:

commit 0d55a99d5c0b9f1d8c9497775dd69929e5091eaa
Merge: 73a203ac8 d4a4bda64
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 16:25:36 2023 +0400

    Merge remote-tracking branch 'origin/master' into http-yaml-conf

commit 73a203ac8acf083fa289015e1f301d05bf320ea7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 16:21:48 2023 +0400

    home: imp docs

commit a4819ace94bfe4427f70f1b8341c9babc9234740
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Jun 29 11:45:30 2023 +0400

    snap: imp script

commit b0913c7ac5c6c46d6a73790fd57d8c5f9d7ace75
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 17:34:03 2023 +0400

    all: docs

commit 14820d6d56f958081d9f236277fd34f356bdab33
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 13:21:43 2023 +0400

    home: imp tests

commit 9db800d3ce39c36da7959e37b4a46736f4217e5c
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 13:17:34 2023 +0400

    all: docs

commit 9174a0ae710da51d85b4e1b1af79eda6a61dd3a2
Merge: ca8c4ae95 d88181343
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 10:19:01 2023 +0400

    Merge remote-tracking branch 'origin/master' into http-yaml-conf

    # Conflicts:
    #	CHANGELOG.md
    #	internal/home/upgrade.go
    #	internal/home/upgrade_test.go

commit ca8c4ae954ece25d78ef2f873bb3ba71fa4b8fa9
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 10:07:15 2023 +0400

    snap: imp script

commit d84473f8e07b2c6e65023613eb4032fd01951521
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Jun 28 09:59:57 2023 +0400

    snap: imp script

commit 8a0808e42ddbff7d9d3345d758f91b14bb4453be
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 15:03:53 2023 +0400

    home: http conf

commit e8fbb89cc5748f9d8fa4be9e702756bd8b869de9
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 14:59:37 2023 +0400

    home: imp code

commit 46541aabc421118562d564675dfd7e594d2056aa
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 12:36:14 2023 +0400

    snap: bind port

commit cecda5fcfd8c473db42f235b4f586b2193086997
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 12:12:39 2023 +0400

    docker: bind port

commit 8d8945b70366c6b018616a32421c77eb281a6ea1
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 11:06:32 2023 +0400

    home: imp code

commit ae5e8c1c4333d7b752c08605d80e41f55ee50e59
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Jun 27 11:02:09 2023 +0400

    home: imp code

commit c9ee460f37e32941b84ea5fa94d21b186d6dd82b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 26 17:11:10 2023 +0400

    home: imp code

commit 44c72445112ef38d6ec9c25b197c119edd6c959f
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 26 11:52:19 2023 +0400

    all: docs

commit e3bf5faeb748f347b1202a496788739ff9219ed0
Merge: 38cc0f639 e7e638443
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 26 11:39:12 2023 +0400

    Merge remote-tracking branch 'origin/master' into http-yaml-conf

commit 38cc0f6399040f1fa39d9da31ad6db65a6bdd4cc
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 26 11:38:17 2023 +0400

    snap: bind port

commit 3b9cb9e8cc89a67e55cecc7a2040c150f8675b4c
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Jun 26 11:25:03 2023 +0400

    docker: bind port

... and 4 more commits
2023-06-29 15:29:52 +03:00
Ainar Garipov
d4a4bda645 Pull request 1897: nextapi-write-conf
Squashed commit of the following:

commit 72f25ffe73d6b8216b01e590fba66fb5f6944113
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 28 21:29:04 2023 +0300

    next: add conf writing, validation
2023-06-29 14:34:06 +03:00
70 changed files with 3367 additions and 1413 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.db
*.log
*.snap
*.test
/agh-backup/
/bin/
/build/*

View File

@@ -14,15 +14,86 @@ and this project adheres to
<!--
## [v0.108.0] - TBA
## [v0.107.33] - 2023-06-28 (APPROX.)
## [v0.107.34] - 2023-07-26 (APPROX.)
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
See also the [v0.107.34 GitHub milestone][ms-v0.107.34].
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
[ms-v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/milestone/69?closed=1
NOTE: Add new changes BELOW THIS COMMENT.
-->
### Changed
- Improved CPU and RAM consumption during updates of filtering-rule lists.
#### Configuration Changes
In this release, the schema version has changed from 23 to 24.
- Properties starting with `log_`, and `verbose` property, which used to set up
logging are now moved to the new object `log` containing new properties `file`,
`max_backups`, `max_size`, `max_age`, `compress`, `local_time`, and `verbose`:
```yaml
# BEFORE:
'log_file': ""
'log_max_backups': 0
'log_max_size': 100
'log_max_age': 3
'log_compress': false
'log_localtime': false
'verbose': false
# AFTER:
'log':
'file': ""
'max_backups': 0
'max_size': 100
'max_age': 3
'compress': false
'local_time': false
'verbose': false
```
To rollback this change, remove the new object `log`, set back `log_` and
`verbose` properties and change the `schema_version` back to `23`.
### Deprecated
- Default exposure of the non-standard ports 784 and 8853 for DNS-over-QUIC in
the `Dockerfile`.
### Fixed
- Two unspecified IPs when a host is blocked in two filter lists ([#5972]).
- Incorrect setting of Parental Control cache size.
- Excessive RAM and CPU consumption by Safe Browsing and Parental Control
filters ([#5896]).
### Removed
- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in
`Dockerfile` ([#5939]). They caused a lot of issues, especially with tools
like `docker-compose` and `podman`.
**NOTE:** Some Docker tools may cache `ENTRYPOINT` sections, so some users may
be required to backup their configuration, stop the container, purge the old
image, and reload it from scratch.
[#5896]: https://github.com/AdguardTeam/AdGuardHome/issues/5896
[#5972]: https://github.com/AdguardTeam/AdGuardHome/issues/5972
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.33] - 2023-07-03
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
### Added
- The new command-line flag `--web-addr` is the address to serve the web UI on,
@@ -37,8 +108,27 @@ NOTE: Add new changes BELOW THIS COMMENT.
#### Configuration Changes
In this release, the schema version has changed from 20 to 22.
In this release, the schema version has changed from 20 to 23.
- Properties `bind_host`, `bind_port`, and `web_session_ttl` which used to setup
web UI binding configuration, are now moved to a new object `http` containing
new properties `address` and `session_ttl`:
```yaml
# BEFORE:
'bind_host': '1.2.3.4'
'bind_port': 8080
'web_session_ttl': 720
# AFTER:
'http':
'address': '1.2.3.4:8080'
'session_ttl': '720h'
```
Note that the new `http.session_ttl` property is now a duration string. To
rollback this change, remove the new object `http`, set back `bind_host`,
`bind_port`, `web_session_ttl`, and change the `schema_version` back to `22`.
- Property `clients.persistent.blocked_services`, which in schema versions 21
and earlier used to be a list containing ids of blocked services, is now an
object containing ids and schedule for blocked services:
@@ -118,14 +208,20 @@ In this release, the schema version has changed from 20 to 22.
### Deprecated
- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in
`Dockerfile` ([#5939]). They cause a lot of issues, especially with tools
like `docker-compose` and `podman`, and will be removed in a future release.
- Flags `-h`, `--host`, `-p`, `--port` have been deprecated. The `-h` flag
will work as an alias for `--help`, instead of the deprecated `--host` in the
future releases.
### Fixed
- Ignoring of `/etc/hosts` file when resolving the hostnames of upstream DNS
servers ([#5902]).
- Excessive error logging when using DNS-over-QUIC ([#5285]).
- Cannot set `bind_host` in AdGuardHome.yaml (docker version)([#4231], [#4235]).
- Inability to set `bind_host` in `AdGuardHome.yaml` in Docker ([#4231],
[#4235]).
- The blocklists can now be deleted properly ([#5700]).
- Queries with the question-section target `.`, for example `NS .`, are now
counted in the statistics and correctly shown in the query log ([#5910]).
@@ -138,12 +234,12 @@ In this release, the schema version has changed from 20 to 22.
[#4235]: https://github.com/AdguardTeam/AdGuardHome/pull/4235
[#5285]: https://github.com/AdguardTeam/AdGuardHome/issues/5285
[#5700]: https://github.com/AdguardTeam/AdGuardHome/issues/5700
[#5902]: https://github.com/AdguardTeam/AdGuardHome/issues/5902
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
[#5939]: https://github.com/AdguardTeam/AdGuardHome/discussions/5939
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
@@ -2125,11 +2221,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...HEAD
[v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.34...HEAD
[v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...v0.107.34
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...HEAD
[v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33
[v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...v0.107.32
[v0.107.31]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...v0.107.31
[v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30

View File

@@ -78,7 +78,7 @@ build: deps quick-build
quick-build: js-build go-build
ci: deps test
ci: deps test go-bench go-fuzz
deps: js-deps go-deps
lint: js-lint go-lint
@@ -104,8 +104,10 @@ js-deps:
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
js-test: ; $(NPM) $(NPM_FLAGS) run test
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh

View File

@@ -1,5 +1,5 @@
{
"client_settings": "Cài đặt máy khách",
"client_settings": "Cài đặt thiết bị",
"example_upstream_reserved": "ngược dòng <0>cho các miền cụ thể</0>;",
"example_upstream_comment": "một lời bình luận.",
"upstream_parallel": "Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến",

View File

@@ -513,6 +513,7 @@
"statistics_clear_confirm": "您確定要清除統計資料嗎?",
"statistics_retention_confirm": "您確定要更改統計資料保存時間嗎?如果您縮短期限部分資料可能將會遺失",
"statistics_cleared": "已清除統計資料",
"statistics_enable": "啟用統計數據",
"interval_hours": "{{count}} 小時",
"interval_hours_plural": "{{count}} 小時",
"filters_configuration": "過濾器設定",

View File

@@ -6,7 +6,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import Card from '../ui/Card';
import { formatNumber } from '../../helpers/helpers';
import LogsSearchLink from '../ui/LogsSearchLink';
import { RESPONSE_FILTER } from '../../helpers/constants';
import { RESPONSE_FILTER, DAY } from '../../helpers/constants';
import Tooltip from '../ui/Tooltip';
const Row = ({
@@ -54,12 +54,12 @@ const Counters = ({ refreshButton, subtitle }) => {
avgProcessingTime,
} = useSelector((state) => state.stats, shallowEqual);
const { t } = useTranslation();
const days = interval / DAY;
const rows = [
{
label: 'dns_query',
count: numDnsQueries,
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
tooltipTitle: days === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: days }),
response_status: RESPONSE_FILTER.ALL.QUERY,
},
{

View File

@@ -1,5 +1,5 @@
{
"timeUpdated": "2023-06-26T13:46:24.414Z",
"timeUpdated": "2023-07-01T00:11:37.465Z",
"categories": {
"0": "audio_video_player",
"1": "comments",
@@ -3348,6 +3348,13 @@
"url": "https://www.microsoft.com/",
"companyId": "microsoft"
},
"binge": {
"name": "Binge",
"categoryId": 0,
"url": "https://binge.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"binlayer": {
"name": "BinLayer",
"categoryId": 4,
@@ -7164,6 +7171,13 @@
"url": "http://flagcounter.com/",
"companyId": "flag_counter"
},
"flash": {
"name": "Flash",
"categoryId": 0,
"url": "https://flashnews.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"flashtalking": {
"name": "Flashtalking",
"categoryId": 4,
@@ -7369,6 +7383,13 @@
"url": "https://publishers.foxaudiencenetwork.com/",
"companyId": "fox_audience_network"
},
"fox_sports": {
"name": "Fox Sports",
"categoryId": 0,
"url": "https://foxsports.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"foxnews_static": {
"name": "Fox News CDN",
"categoryId": 9,
@@ -7381,6 +7402,13 @@
"url": "https://www.foxpush.com/",
"companyId": "foxpush"
},
"foxtel": {
"name": "Foxtel",
"categoryId": 0,
"url": "https://foxtel.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"foxydeal_com": {
"name": "foxydeal.com",
"categoryId": 12,
@@ -7983,12 +8011,40 @@
"url": "http://www.google.com",
"companyId": "google"
},
"google_auth": {
"name": "Google Auth",
"categoryId": 2,
"url": "https://myaccount.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_beacons": {
"name": "Google Beacons",
"categoryId": 6,
"url": "https://google.xyz",
"companyId": "google"
},
"google_chat": {
"name": "Google Chat",
"categoryId": 7,
"url": "https://mail.google.com/chat/",
"companyId": "google",
"source": "AdGuard"
},
"google_cloud_platform": {
"name": "Google Cloud Platform",
"categoryId": 10,
"url": "https://cloud.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_cloud_storage": {
"name": "Google Cloud Storage",
"categoryId": 10,
"url": "https://cloud.google.com/storage/",
"companyId": "google",
"source": "AdGuard"
},
"google_custom_search": {
"name": "Google Custom Search Ads",
"categoryId": 4,
@@ -8001,6 +8057,27 @@
"url": "https://programmablesearchengine.google.com/about/",
"companyId": "google"
},
"google_dns": {
"name": "Google DNS",
"categoryId": 10,
"url": "hhttps://dns.google/",
"companyId": "google",
"source": "AdGuard"
},
"google_domains": {
"name": "Google Domains",
"categoryId": 10,
"url": "https://domains.google/",
"companyId": "google",
"source": "AdGuard"
},
"google_edge": {
"name": "Google Edge CDN",
"categoryId": 9,
"url": "https://peering.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_email": {
"name": "Google Email",
"categoryId": 13,
@@ -8013,12 +8090,47 @@
"url": "https://fonts.google.com/",
"companyId": "google"
},
"google_hosted": {
"name": "Google Hosted",
"categoryId": 10,
"url": "https://workspace.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_ima": {
"name": "Google IMA",
"categoryId": 4,
"url": "http://www.google.com",
"companyId": "google"
},
"google_location": {
"name": "Google Location",
"categoryId": 8,
"url": "https://patents.google.com/patent/WO2007025143A1/",
"companyId": "google",
"source": "AdGuard"
},
"google_maps": {
"name": "Google Maps",
"categoryId": 2,
"url": "https://www.google.com/maps/",
"companyId": "google",
"source": "AdGuard"
},
"google_marketing": {
"name": "Google Marketing",
"categoryId": 6,
"url": "https://marketingplatform.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_meet": {
"name": "Google Meet",
"categoryId": 2,
"url": "https://meet.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_photos": {
"name": "Google Photos",
"categoryId": 9,
@@ -8031,6 +8143,13 @@
"url": "http://www.google.com",
"companyId": "google"
},
"google_play": {
"name": "Google Play",
"categoryId": 8,
"url": "https://play.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_plus": {
"name": "Google+ Platform",
"categoryId": 7,
@@ -8110,6 +8229,13 @@
"url": "http://www.google.com",
"companyId": "google"
},
"google_voice": {
"name": "Google Voice",
"categoryId": 2,
"url": "https://voice.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"google_website_optimizer": {
"name": "Google Website Optimizer",
"categoryId": 6,
@@ -8122,6 +8248,13 @@
"url": "http://www.google.com",
"companyId": "google"
},
"google_workspace": {
"name": "Google Workspace",
"categoryId": 2,
"url": "https://workspace.google.com/",
"companyId": "google",
"source": "AdGuard"
},
"googleapis.com": {
"name": "Google APIs",
"categoryId": 9,
@@ -9766,6 +9899,13 @@
"url": "http://kavanga.ru/",
"companyId": "kavanga"
},
"kayo_sports": {
"name": "Kayo Sports",
"categoryId": 0,
"url": "https://kayosports.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"keen_io": {
"name": "Keen IO",
"categoryId": 6,
@@ -13093,6 +13233,13 @@
"url": "http://perfectmarket.com/",
"companyId": "perfect_market"
},
"perfops": {
"name": "PerfOps",
"categoryId": 6,
"url": "https://perfops.net/",
"companyId": "perfops",
"source": "AdGuard"
},
"perform_group": {
"name": "Perform Group",
"categoryId": 5,
@@ -16484,6 +16631,13 @@
"url": "http://www.streak.com/",
"companyId": "streak"
},
"streamotion": {
"name": "Streamotion",
"categoryId": 0,
"url": "https://streamotion.com.au/",
"companyId": "foxtel",
"source": "AdGuard"
},
"streamrail.com": {
"name": "StreamRail",
"categoryId": 4,
@@ -20598,6 +20752,7 @@
"bing.com": "bing_ads",
"bing.net": "bing_ads",
"virtualearth.net": "bing_maps",
"binge.com.au": "binge",
"view.binlayer.com": "binlayer",
"widgets.binotel.com": "binotel",
"esendra.fi": "bisnode",
@@ -21212,6 +21367,7 @@
"2mdn.net": "doubleclick",
"doubleclick.net": "doubleclick",
"invitemedia.com": "doubleclick",
"doubleclick.com": "doubleclick",
"doublepimp.com": "doublepimp",
"doublepimpssl.com": "doublepimp",
"redcourtside.com": "doublepimp",
@@ -21435,12 +21591,28 @@
"findizer.fr": "findizer.fr",
"findologic.com": "findologic.com",
"app-measurement.com": "firebase",
"fcm.googleapis.com": "firebase",
"firebaseappcheck.googleapis.com": "firebase",
"firebaseapp.com": "firebase",
"firebase.com": "firebase",
"firebasedynamiclinks.googleapis.com": "firebase",
"firebasedynamiclinks-ipv4.googleapis.com": "firebase",
"firebasedynamiclinks-ipv6.googleapis.com": "firebase",
"firebase.googleapis.com": "firebase",
"firebase.google.com": "firebase",
"firebaseinappmessaging.googleapis.com": "firebase",
"firebaseinstallations.googleapis.com": "firebase",
"firebaselogging.googleapis.com": "firebase",
"firebaselogging-pa.googleapis.com": "firebase",
"firebaseperusertopics-pa.googleapis.com": "firebase",
"firebaseremoteconfig.googleapis.com": "firebase",
"firebaseio.com": "firebaseio.com",
"firstimpression.io": "first_impression",
"fitanalytics.com": "fit_analytics",
"fivetran.com": "fivetran",
"flagads.net": "flag_ads",
"flagcounter.com": "flag_counter",
"flashnews.com.au": "flash",
"flashtalking.com": "flashtalking",
"flattr.com": "flattr_button",
"flexlinks.com": "flexoffers",
@@ -21486,9 +21658,11 @@
"platform.foursquare.com": "foursquare_widget",
"fout.jp": "fout.jp",
"fimserve.com": "fox_audience_network",
"foxsports.com.au": "fox_sports",
"fncstatic.com": "foxnews_static",
"cdn.foxpush.net": "foxpush",
"foxpush.com": "foxpush",
"foxtel.com.au": "foxtel",
"foxydeal.com": "foxydeal_com",
"yabidos.com": "fraudlogix",
"besucherstatistiken.com": "free_counter",
@@ -21649,15 +21823,287 @@
"google.ru": "google",
"google.se": "google",
"google.tn": "google",
"1e100.net": "google",
"agnss.goog": "google",
"channel.status.request.url": "google",
"g.cn": "google",
"g.co": "google",
"google.ad": "google",
"google.ae": "google",
"google.al": "google",
"google.am": "google",
"googleapis.cn": "google",
"google.as": "google",
"google.az": "google",
"google.ba": "google",
"google.bf": "google",
"google.bg": "google",
"google.bi": "google",
"google.bj": "google",
"google.bs": "google",
"google.bt": "google",
"google.by": "google",
"google.cat": "google",
"google.cd": "google",
"google.cf": "google",
"google.cg": "google",
"google.ci": "google",
"google.cl": "google",
"google.cm": "google",
"google.cn": "google",
"google.co.ao": "google",
"google.co.bw": "google",
"google.co.ck": "google",
"google.co.cr": "google",
"googlecode.com": "google",
"google.co.il": "google",
"google.co.ke": "google",
"google.co.kr": "google",
"google.co.ls": "google",
"google.com.af": "google",
"google.com.ag": "google",
"google.com.ai": "google",
"google.com.bd": "google",
"google.com.bh": "google",
"google.com.bn": "google",
"google.com.bo": "google",
"google.com.bz": "google",
"google.com.co": "google",
"google.com.cu": "google",
"google.com.cy": "google",
"google.com.ec": "google",
"google.com.eg": "google",
"google.com.et": "google",
"google.com.fj": "google",
"google.com.gh": "google",
"google.com.gi": "google",
"google.com.gt": "google",
"google.com.hk": "google",
"google.com.jm": "google",
"google.com.kh": "google",
"google.com.kw": "google",
"google.com.lb": "google",
"google.com.my": "google",
"google.com.na": "google",
"google.com.nf": "google",
"google.com.ng": "google",
"google.com.ni": "google",
"google.com.np": "google",
"google.com.om": "google",
"google.com.pa": "google",
"google.com.pe": "google",
"google.com.pg": "google",
"google.com.ph": "google",
"google.com.pk": "google",
"google.com.pr": "google",
"google.com.py": "google",
"google.com.qa": "google",
"google.com.sa": "google",
"google.com.sb": "google",
"google.com.sg": "google",
"google.com.sl": "google",
"google.com.sv": "google",
"google.com.tj": "google",
"google.com.uy": "google",
"google.com.vc": "google",
"google.com.vn": "google",
"google.co.mz": "google",
"google.co.nz": "google",
"google.co.tz": "google",
"google.co.ug": "google",
"google.co.uz": "google",
"google.co.ve": "google",
"google.co.vi": "google",
"google.co.za": "google",
"google.co.zm": "google",
"google.co.zw": "google",
"google.cv": "google",
"google.dj": "google",
"google.dm": "google",
"googledownloads.cn": "google",
"google.ee": "google",
"google.fm": "google",
"google.ga": "google",
"google.ge": "google",
"google.gg": "google",
"google.gl": "google",
"google.gm": "google",
"google.gp": "google",
"google.gy": "google",
"google.hn": "google",
"google.hr": "google",
"google.ht": "google",
"google.im": "google",
"google.in": "google",
"google.iq": "google",
"google.is": "google",
"google.je": "google",
"google.jo": "google",
"google.kg": "google",
"google.ki": "google",
"google.kz": "google",
"google.la": "google",
"google.li": "google",
"google.lk": "google",
"google.lt": "google",
"google.lu": "google",
"google.lv": "google",
"google.md": "google",
"google.me": "google",
"google.mg": "google",
"google.mk": "google",
"google.ml": "google",
"google.mn": "google",
"google.ms": "google",
"google.mu": "google",
"google.mv": "google",
"google.mw": "google",
"google.ne": "google",
"google.net": "google",
"google.nr": "google",
"google.nu": "google",
"googleoptimize.com": "google",
"google.org": "google",
"google.pn": "google",
"google.ps": "google",
"google.rw": "google",
"google.sc": "google",
"google.sh": "google",
"google.si": "google",
"google.sk": "google",
"google.sm": "google",
"google.sn": "google",
"google.so": "google",
"google.sr": "google",
"google.st": "google",
"google.td": "google",
"google.tg": "google",
"google.tk": "google",
"google.tl": "google",
"google.tm": "google",
"google.to": "google",
"google.tt": "google",
"google.us": "google",
"google.vg": "google",
"google.vu": "google",
"googleweblight.in": "google",
"google.ws": "google",
"googlezip.net": "google",
"gstatic.cn": "google",
"news.google.com": "google",
"oo.gl": "google",
"withgoogle.com": "google",
"googleadservices.com": "google_adservices",
"google-analytics.com": "google_analytics",
"ssl-google-analytics.l.google.com": "google_analytics",
"www-googletagmanager.l.google.com": "google_analytics",
"appspot.com": "google_appspot",
"googlehosted.com": "google_appspot",
"accounts.google.com": "google_auth",
"myaccount.google.com": "google_auth",
"oauth2.googleapis.com": "google_auth",
"ogs.google.com": "google_auth",
"securetoken.googleapis.com": "google_auth",
"beacons-google.com": "google_beacons",
"alt1-mtalk.google.com": "google_chat",
"alt2-mtalk.google.com": "google_chat",
"alt3-mtalk.google.com": "google_chat",
"alt4-mtalk.google.com": "google_chat",
"alt5-mtalk.google.com": "google_chat",
"alt6-mtalk.google.com": "google_chat",
"alt7-mtalk.google.com": "google_chat",
"alt8-mtalk.google.com": "google_chat",
"chat.google.com": "google_chat",
"mobile-gtalk4.l.google.com": "google_chat",
"mobile-gtalk.l.google.com": "google_chat",
"mtalk4.google.com": "google_chat",
"mtalk.google.com": "google_chat",
"talk.google.com": "google_chat",
"talk.l.google.com": "google_chat",
"talkx.l.google.com": "google_chat",
"cloud.google.com": "google_cloud_platform",
"gcp.gvt2.com": "google_cloud_platform",
"storage.googleapis.com": "google_cloud_storage",
"adsensecustomsearchads.com": "google_custom_search",
"dns.google": "google_dns",
"dns.google.com": "google_dns",
"google-public-dns-a.google.com": "google_dns",
"google-public-dns-b.google.com": "google_dns",
"domains.google": "google_domains",
"googledomains.com": "google_domains",
"nic.google": "google_domains",
"registry.google": "google_domains",
"edge.google.com": "google_edge",
"mail-ads.google.com": "google_email",
"fonts.googleapis.com": "google_fonts",
"cloudfunctions.net": "google_hosted",
"ghs46.googlehosted.com": "google_hosted",
"ghs4.googlehosted.com": "google_hosted",
"ghs6.googlehosted.com": "google_hosted",
"ghs.googlehosted.com": "google_hosted",
"googlehosted.l.googleusercontent.com": "google_hosted",
"run.app": "google_hosted",
"supl.google.com": "google_location",
"earth.app.goo.gl": "google_maps",
"geo0.ggpht.com": "google_maps",
"geo1.ggpht.com": "google_maps",
"geo2.ggpht.com": "google_maps",
"geo3.ggpht.com": "google_maps",
"kh.google.com": "google_maps",
"maps.app.goo.gl": "google_maps",
"maps.google.ca": "google_maps",
"maps.google.ch": "google_maps",
"maps.google.co.jp": "google_maps",
"maps.google.com": "google_maps",
"maps.google.com.mx": "google_maps",
"maps.google.co.uk": "google_maps",
"maps.google.es": "google_maps",
"maps.google.se": "google_maps",
"maps.gstatic.com": "google_maps",
"adsense.google.com": "google_marketing",
"adservice.google.ca": "google_marketing",
"adservice.google.co.in": "google_marketing",
"adservice.google.co.kr": "google_marketing",
"adservice.google.com": "google_marketing",
"adservice.google.com.ar": "google_marketing",
"adservice.google.com.au": "google_marketing",
"adservice.google.com.br": "google_marketing",
"adservice.google.com.co": "google_marketing",
"adservice.google.com.gt": "google_marketing",
"adservice.google.com.mx": "google_marketing",
"adservice.google.com.pe": "google_marketing",
"adservice.google.com.ph": "google_marketing",
"adservice.google.com.pk": "google_marketing",
"adservice.google.com.tr": "google_marketing",
"adservice.google.com.tw": "google_marketing",
"adservice.google.com.vn": "google_marketing",
"adservice.google.co.uk": "google_marketing",
"adservice.google.co.za": "google_marketing",
"adservice.google.de": "google_marketing",
"adservice.google.dk": "google_marketing",
"adservice.google.es": "google_marketing",
"adservice.google.fr": "google_marketing",
"adservice.google.nl": "google_marketing",
"adservice.google.no": "google_marketing",
"adservice.google.pl": "google_marketing",
"adservice.google.ru": "google_marketing",
"adservice.google.vg": "google_marketing",
"dai.google.com": "google_marketing",
"doubleclickbygoogle.com": "google_marketing",
"googlesyndication-cn.com": "google_marketing",
"duo.google.com": "google_meet",
"hangouts.clients6.google.com": "google_meet",
"hangouts.googleapis.com": "google_meet",
"hangouts.google.com": "google_meet",
"meet.google.com": "google_meet",
"meetings.googleapis.com": "google_meet",
"stun1.l.google.com": "google_meet",
"stun.l.google.com": "google_meet",
"ggpht.com": "google_photos",
"play-fe.googleapis.com": "google_play",
"play.googleapis.com": "google_play",
"play.google.com": "google_play",
"play-lh.googleusercontent.com": "google_play",
"1e100cdn.net": "google_servers",
"gvt1.com": "google_servers",
"gvt2.com": "google_servers",
@@ -21670,7 +22116,22 @@
"pki.goog": "google_trust_services",
"googlecommerce.com": "google_trusted_stores",
"googleusercontent.com": "google_users",
"telephony.goog": "google_voice",
"voice.google.com": "google_voice",
"gmodules.com": "google_widgets",
"calendar.google.com": "google_workspace",
"contacts.google.com": "google_workspace",
"currents.google.com": "google_workspace",
"docs.google.com": "google_workspace",
"drive.google.com": "google_workspace",
"forms.google.com": "google_workspace",
"gsuite.google.com": "google_workspace",
"jamboard.google.com": "google_workspace",
"keep.google.com": "google_workspace",
"plus.google.com": "google_workspace",
"sheets.google.com": "google_workspace",
"slides.google.com": "google_workspace",
"spreadsheets.google.com": "google_workspace",
"googleapis.com": "googleapis.com",
"gooal.herokuapp.com": "goooal",
"gooo.al": "goooal",
@@ -22037,6 +22498,7 @@
"cen.katchup.fr": "katchup",
"kau.li": "kauli",
"kavanga.ru": "kavanga",
"kayosports.com.au": "kayo_sports",
"dc8na2hxrj29i.cloudfront.net": "keen_io",
"keen.io": "keen_io",
"widget.kelkoo.com": "kelkoo",
@@ -22847,6 +23309,7 @@
"perfectaudience.com": "perfect_audience",
"prfct.co": "perfect_audience",
"perfectmarket.com": "perfect_market",
"perfops.io": "perfops",
"performgroup.com": "perform_group",
"analytics.performable.com": "performable",
"performancing.com": "performancing_metrics",
@@ -23652,6 +24115,7 @@
"bizsolutions.strands.com": "strands_recommender",
"strava.com": "strava",
"mailfoogae.appspot.com": "streak",
"streamotion.com.au": "streamotion",
"streamrail.com": "streamrail.com",
"streamrail.net": "streamrail.com",
"stridespark.com": "stride",

View File

@@ -1,6 +1,6 @@
# A docker file for scripts/make/build-docker.sh.
FROM alpine:3.17
FROM alpine:3.18
ARG BUILD_DATE
ARG VERSION
@@ -25,8 +25,6 @@ RUN apk --no-cache add ca-certificates libcap tzdata && \
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
chown -R nobody: /opt/adguardhome
RUN apk --no-cache add tini
ARG DIST_DIR
ARG TARGETARCH
ARG TARGETOS
@@ -43,43 +41,24 @@ RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
# 68 : UDP : DHCP (client)
# 80 : TCP : HTTP (main)
# 443 : TCP, UDP : HTTPS, DNS-over-HTTPS (incl. HTTP/3), DNSCrypt (main)
# 784 : UDP : DNS-over-QUIC (experimental)
# 784 : UDP : DNS-over-QUIC (deprecated; use 853)
# 853 : TCP, UDP : DNS-over-TLS, DNS-over-QUIC
# 3000 : TCP, UDP : HTTP(S) (alt, incl. HTTP/3)
# 3001 : TCP, UDP : HTTP(S) (beta, incl. HTTP/3)
# 5443 : TCP, UDP : DNSCrypt (alt)
# 6060 : TCP : HTTP (pprof)
# 8853 : UDP : DNS-over-QUIC (experimental)
# 8853 : UDP : DNS-over-QUIC (deprecated; use 853)
#
# TODO(a.garipov): Remove the old, non-standard 784 and 8853 ports for
# DNS-over-QUIC in a future release.
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 784/udp\
853/tcp 853/udp 3000/tcp 3000/udp 5443/tcp\
5443/udp 6060/tcp 8853/udp
853/tcp 853/udp 3000/tcp 3000/udp 5443/tcp 5443/udp 6060/tcp\
8853/udp
WORKDIR /opt/adguardhome/work
# Install helpers for healthcheck.
COPY --chown=nobody:nogroup\
./${DIST_DIR}/docker/scripts\
/opt/adguardhome/scripts
HEALTHCHECK \
--interval=30s \
--timeout=10s \
--retries=3 \
CMD [ "/opt/adguardhome/scripts/healthcheck.sh" ]
# It seems that the healthckech script sometimes spawns zombie processes, so we
# need a way to handle them, since AdGuard Home doesn't know how to keep track
# of the processes delegated to it by the OS. Use tini as entry point because
# it needs the PID=1 to be the default parent for orphaned processes.
#
# See https://github.com/adguardTeam/adGuardHome/issues/3290.
ENTRYPOINT [ "/sbin/tini", "--" ]
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD [ \
"/opt/adguardhome/AdGuardHome", \
"--no-check-update", \
"-c", "/opt/adguardhome/conf/AdGuardHome.yaml", \
"-w", "/opt/adguardhome/work" \

View File

@@ -1,29 +0,0 @@
/^[^[:space:]]/ { is_dns = /^dns:/ }
/^[[:space:]]+bind_hosts:/ { if (is_dns) prev_line = FNR }
/^[[:space:]]+- .+/ {
if (FNR - prev_line == 1) {
addrs[$2] = true
prev_line = FNR
if ($2 == "0.0.0.0" || $2 == "'::'") {
# Drop all the other addresses.
delete addrs
addrs[""] = true
prev_line = -1
}
}
}
/^[[:space:]]+port:/ { if (is_dns) port = $2 }
END {
for (addr in addrs) {
if (match(addr, ":")) {
print "[" addr "]:" port
} else {
print addr ":" port
}
}
}

View File

@@ -1,107 +0,0 @@
#!/bin/sh
# AdGuard Home Docker healthcheck script
# Exit the script if a pipeline fails (-e), prevent accidental filename
# expansion (-f), and consider undefined variables as errors (-u).
set -e -f -u
# Function error_exit is an echo wrapper that writes to stderr and stops the
# script execution with code 1.
error_exit() {
echo "$1" 1>&2
exit 1
}
agh_dir="/opt/adguardhome"
readonly agh_dir
filename="${agh_dir}/conf/AdGuardHome.yaml"
readonly filename
if ! [ -f "$filename" ]
then
wget "http://127.0.0.1:3000" -O /dev/null -q || exit 1
exit 0
fi
help_dir="${agh_dir}/scripts"
readonly help_dir
# Parse web host
web_url="$( awk -f "${help_dir}/web-bind.awk" "$filename" )"
readonly web_url
if [ "$web_url" = '' ]
then
error_exit "no web bindings could be retrieved from $filename"
fi
# TODO(e.burkov): Deal with 0 port.
case "$web_url"
in
(*':0')
error_exit '0 in web port is not supported by healthcheck'
;;
(*)
# Go on.
;;
esac
# Parse DNS hosts
dns_hosts="$( awk -f "${help_dir}/dns-bind.awk" "$filename" )"
readonly dns_hosts
if [ "$dns_hosts" = '' ]
then
error_exit "no DNS bindings could be retrieved from $filename"
fi
first_dns="$( echo "$dns_hosts" | head -n 1 )"
readonly first_dns
# TODO(e.burkov): Deal with 0 port.
case "$first_dns"
in
(*':0')
error_exit '0 in DNS port is not supported by healthcheck'
;;
(*)
# Go on.
;;
esac
# Check
# Skip SSL certificate validation since there is no guarantee the container
# trusts the one used. It should be safe to drop the SSL validation since the
# current script intended to be used from inside the container and only checks
# the endpoint availability, ignoring the content of the response.
#
# See https://github.com/AdguardTeam/AdGuardHome/issues/5642.
wget --no-check-certificate "$web_url" -O /dev/null -q || exit 1
test_fqdn="healthcheck.adguardhome.test."
readonly test_fqdn
# The awk script currently returns only port prefixed with colon in case of
# unspecified address.
case "$first_dns"
in
(':'*)
nslookup -type=a "$test_fqdn" "127.0.0.1${first_dns}" > /dev/null ||\
nslookup -type=a "$test_fqdn" "[::1]${first_dns}" > /dev/null ||\
error_exit "nslookup failed for $host"
;;
(*)
echo "$dns_hosts" | while read -r host
do
nslookup -type=a "$test_fqdn" "$host" > /dev/null ||\
error_exit "nslookup failed for $host"
done
;;
esac

View File

@@ -1,13 +0,0 @@
# Don't consider the HTTPS hostname since the enforced HTTPS redirection should
# work if the SSL check skipped. See file docker/healthcheck.sh.
/^bind_host:/ { host = $2 }
/^bind_port:/ { port = $2 }
END {
if (match(host, ":")) {
print "http://[" host "]:" port
} else {
print "http://" host ":" port
}
}

View File

@@ -56,15 +56,20 @@ func (rm *requestMatcher) MatchRequest(
) (res *urlfilter.DNSResult, ok bool) {
switch req.DNSType {
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
log.Debug("%s: handling the request for %s", hostsContainerPrefix, req.Hostname)
log.Debug(
"%s: handling %s request for %s",
hostsContainerPrefix,
dns.Type(req.DNSType),
req.Hostname,
)
rm.stateLock.RLock()
defer rm.stateLock.RUnlock()
return rm.engine.MatchRequest(req)
default:
return nil, false
}
rm.stateLock.RLock()
defer rm.stateLock.RUnlock()
return rm.engine.MatchRequest(req)
}
// Translate returns the source hosts-syntax rule for the generated dnsrewrite
@@ -96,6 +101,8 @@ const hostsContainerPrefix = "hosts container"
// HostsContainer stores the relevant hosts database provided by the OS and
// processes both A/AAAA and PTR DNS requests for those.
//
// TODO(e.burkov): Improve API and move to golibs.
type HostsContainer struct {
// requestMatcher matches the requests and translates the rules. It's
// embedded to implement MatchRequest and Translate for *HostsContainer.

View File

@@ -2,8 +2,8 @@ package aghtest
import (
"context"
"io"
"io/fs"
"net"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
@@ -19,23 +19,23 @@ import (
// Package fs
// type check
var _ fs.FS = &FS{}
// FS is a mock [fs.FS] implementation for tests.
// FS is a fake [fs.FS] implementation for tests.
type FS struct {
OnOpen func(name string) (fs.File, error)
}
// type check
var _ fs.FS = (*FS)(nil)
// Open implements the [fs.FS] interface for *FS.
func (fsys *FS) Open(name string) (fs.File, error) {
return fsys.OnOpen(name)
}
// type check
var _ fs.GlobFS = &GlobFS{}
var _ fs.GlobFS = (*GlobFS)(nil)
// GlobFS is a mock [fs.GlobFS] implementation for tests.
// GlobFS is a fake [fs.GlobFS] implementation for tests.
type GlobFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
@@ -48,9 +48,9 @@ func (fsys *GlobFS) Glob(pattern string) ([]string, error) {
}
// type check
var _ fs.StatFS = &StatFS{}
var _ fs.StatFS = (*StatFS)(nil)
// StatFS is a mock [fs.StatFS] implementation for tests.
// StatFS is a fake [fs.StatFS] implementation for tests.
type StatFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
@@ -62,47 +62,34 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
return fsys.OnStat(name)
}
// Package net
// Package io
// type check
var _ net.Listener = (*Listener)(nil)
// Listener is a mock [net.Listener] implementation for tests.
type Listener struct {
OnAccept func() (conn net.Conn, err error)
OnAddr func() (addr net.Addr)
OnClose func() (err error)
// Writer is a fake [io.Writer] implementation for tests.
type Writer struct {
OnWrite func(b []byte) (n int, err error)
}
// Accept implements the [net.Listener] interface for *Listener.
func (l *Listener) Accept() (conn net.Conn, err error) {
return l.OnAccept()
}
var _ io.Writer = (*Writer)(nil)
// Addr implements the [net.Listener] interface for *Listener.
func (l *Listener) Addr() (addr net.Addr) {
return l.OnAddr()
}
// Close implements the [net.Listener] interface for *Listener.
func (l *Listener) Close() (err error) {
return l.OnClose()
// Write implements the [io.Writer] interface for *Writer.
func (w *Writer) Write(b []byte) (n int, err error) {
return w.OnWrite(b)
}
// Module adguard-home
// Package aghos
// type check
var _ aghos.FSWatcher = (*FSWatcher)(nil)
// FSWatcher is a mock [aghos.FSWatcher] implementation for tests.
// FSWatcher is a fake [aghos.FSWatcher] implementation for tests.
type FSWatcher struct {
OnEvents func() (e <-chan struct{})
OnAdd func(name string) (err error)
OnClose func() (err error)
}
// type check
var _ aghos.FSWatcher = (*FSWatcher)(nil)
// Events implements the [aghos.FSWatcher] interface for *FSWatcher.
func (w *FSWatcher) Events() (e <-chan struct{}) {
return w.OnEvents()
@@ -120,16 +107,16 @@ func (w *FSWatcher) Close() (err error) {
// Package agh
// type check
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
// ServiceWithConfig is a fake [agh.ServiceWithConfig] implementation for tests.
type ServiceWithConfig[ConfigType any] struct {
OnStart func() (err error)
OnShutdown func(ctx context.Context) (err error)
OnConfig func() (c ConfigType)
}
// type check
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
// Start implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Start() (err error) {
@@ -152,10 +139,7 @@ func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
// Package upstream
// type check
var _ upstream.Upstream = (*UpstreamMock)(nil)
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
// UpstreamMock is a fake [upstream.Upstream] implementation for tests.
//
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
// rename it to just Upstream.
@@ -165,6 +149,9 @@ type UpstreamMock struct {
OnClose func() (err error)
}
// type check
var _ upstream.Upstream = (*UpstreamMock)(nil)
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Address() (addr string) {
return u.OnAddress()

View File

@@ -51,6 +51,9 @@ func migrateDB(conf *ServerConfig) (err error) {
oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename)
dataDirPath := filepath.Join(conf.DataDir, dataFilename)
// #nosec G304 -- Trust this path, since it's taken from the old file name
// relative to the working directory and should generally be considered
// safe.
file, err := os.Open(oldLeasesPath)
if errors.Is(err, os.ErrNotExist) {
// Nothing to migrate.

View File

@@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -436,102 +435,6 @@ func (s *Server) initDefaultSettings() {
}
}
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// prepareUpstreamSettings - prepares upstream DNS server settings
func (s *Server) prepareUpstreamSettings() error {
// We're setting a customized set of RootCAs. The reason is that Go default
// mechanism of loading TLS roots does not always work properly on some
// routers so we're loading roots manually and pass it here.
//
// See [aghtls.SystemRootCAs].
upstream.RootCAs = s.conf.TLSv12Roots
upstream.CipherSuites = s.conf.TLSCiphers
// Load upstreams either from the file, or from the settings
var upstreams []string
if s.conf.UpstreamDNSFileName != "" {
data, err := os.ReadFile(s.conf.UpstreamDNSFileName)
if err != nil {
return fmt.Errorf("reading upstream from file: %w", err)
}
upstreams = stringutil.SplitTrimmed(string(data), "\n")
log.Debug("dns: using %d upstream servers from file %s", len(upstreams), s.conf.UpstreamDNSFileName)
} else {
upstreams = s.conf.UpstreamDNS
}
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
upstreamConfig, err := proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
PreferIPv6: s.conf.BootstrapPreferIPv6,
},
)
if err != nil {
return fmt.Errorf("parsing upstream config: %w", err)
}
if len(upstreamConfig.Upstreams) == 0 {
log.Info("warning: no default upstream servers specified, using %v", defaultDNS)
var uc *proxy.UpstreamConfig
uc, err = proxy.ParseUpstreamsConfig(
defaultDNS,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
PreferIPv6: s.conf.BootstrapPreferIPv6,
},
)
if err != nil {
return fmt.Errorf("parsing default upstreams: %w", err)
}
upstreamConfig.Upstreams = uc.Upstreams
}
s.conf.UpstreamConfig = upstreamConfig
return nil
}
// setProxyUpstreamMode sets the upstream mode and related settings in conf
// based on provided parameters.
func setProxyUpstreamMode(
conf *proxy.Config,
allServers bool,
fastestAddr bool,
fastestTimeout time.Duration,
) {
if allServers {
conf.UpstreamMode = proxy.UModeParallel
} else if fastestAddr {
conf.UpstreamMode = proxy.UModeFastestAddr
conf.FastestPingTimeout = fastestTimeout
} else {
conf.UpstreamMode = proxy.UModeLoadBalance
}
}
// prepareIpsetListSettings reads and prepares the ipset configuration either
// from a file or from the data in the configuration file.
func (s *Server) prepareIpsetListSettings() (err error) {
@@ -540,6 +443,7 @@ func (s *Server) prepareIpsetListSettings() (err error) {
return s.ipset.init(s.conf.IpsetList)
}
// #nosec G304 -- Trust the path explicitly given by the user.
data, err := os.ReadFile(fn)
if err != nil {
return err

View File

@@ -17,6 +17,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
@@ -277,17 +278,6 @@ func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
return s.internalProxy.LookupIPAddr(host)
}
// RDNSExchanger is a resolver for clients' addresses.
type RDNSExchanger interface {
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
// or as external.
Exchange(ip net.IP) (host string, err error)
// ResolvesPrivatePTR returns true if the RDNSExchanger is able to
// resolve PTR requests for locally-served addresses.
ResolvesPrivatePTR() (ok bool)
}
const (
// ErrRDNSNoData is returned by [RDNSExchanger.Exchange] when the answer
// section of response is either NODATA or has no PTR records.
@@ -299,10 +289,10 @@ const (
)
// type check
var _ RDNSExchanger = (*Server)(nil)
var _ rdns.Exchanger = (*Server)(nil)
// Exchange implements the RDNSExchanger interface for *Server.
func (s *Server) Exchange(ip net.IP) (host string, err error) {
// Exchange implements the [rdns.Exchanger] interface for *Server.
func (s *Server) Exchange(ip netip.Addr) (host string, err error) {
s.serverLock.RLock()
defer s.serverLock.RUnlock()
@@ -310,7 +300,7 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
return "", nil
}
arpa, err := netutil.IPToReversedAddr(ip)
arpa, err := netutil.IPToReversedAddr(ip.AsSlice())
if err != nil {
return "", fmt.Errorf("reversing ip: %w", err)
}
@@ -335,7 +325,7 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
}
var resolver *proxy.Proxy
if s.privateNets.Contains(ip) {
if s.isPrivateIP(ip) {
if !s.conf.UsePrivateRDNS {
return "", nil
}
@@ -350,8 +340,12 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
return "", err
}
return hostFromPTR(ctx.Res)
}
// hostFromPTR returns domain name from the PTR response or error.
func hostFromPTR(resp *dns.Msg) (host string, err error) {
// Distinguish between NODATA response and a failed request.
resp := ctx.Res
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
return "", fmt.Errorf(
"received %s response: %w",
@@ -370,12 +364,25 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
return "", ErrRDNSNoData
}
// ResolvesPrivatePTR implements the RDNSExchanger interface for *Server.
func (s *Server) ResolvesPrivatePTR() (ok bool) {
// isPrivateIP returns true if the ip is private.
func (s *Server) isPrivateIP(ip netip.Addr) (ok bool) {
return s.privateNets.Contains(ip.AsSlice())
}
// ShouldResolveClient returns false if ip is a loopback address, or ip is
// private and resolving of private addresses is disabled.
func (s *Server) ShouldResolveClient(ip netip.Addr) (ok bool) {
if ip.IsLoopback() {
return false
}
isPrivate := s.isPrivateIP(ip)
s.serverLock.RLock()
defer s.serverLock.RUnlock()
return s.conf.UsePrivateRDNS
return s.conf.ResolveClients &&
(s.conf.UsePrivateRDNS || !isPrivate)
}
// Start starts the DNS server.
@@ -466,19 +473,15 @@ func (s *Server) setupResolvers(localAddrs []string) (err error) {
log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", localAddrs)
var upsConfig *proxy.UpstreamConfig
upsConfig, err = proxy.ParseUpstreamsConfig(
localAddrs,
&upstream.Options{
Bootstrap: bootstraps,
Timeout: defaultLocalTimeout,
// TODO(e.burkov): Should we verify server's certificates?
upsConfig, err := s.prepareUpstreamConfig(localAddrs, nil, &upstream.Options{
Bootstrap: bootstraps,
Timeout: defaultLocalTimeout,
// TODO(e.burkov): Should we verify server's certificates?
PreferIPv6: s.conf.BootstrapPreferIPv6,
},
)
PreferIPv6: s.conf.BootstrapPreferIPv6,
})
if err != nil {
return fmt.Errorf("parsing upstreams: %w", err)
return fmt.Errorf("parsing private upstreams: %w", err)
}
s.localResolvers = &proxy.Proxy{
@@ -510,7 +513,8 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
err = s.prepareUpstreamSettings()
if err != nil {
return fmt.Errorf("preparing upstream settings: %w", err)
// Don't wrap the error, because it's informative enough as is.
return err
}
var proxyConfig proxy.Config

View File

@@ -1273,11 +1273,11 @@ func TestServer_Exchange(t *testing.T) {
)
var (
onesIP = net.IP{1, 1, 1, 1}
localIP = net.IP{192, 168, 1, 1}
onesIP = netip.MustParseAddr("1.1.1.1")
localIP = netip.MustParseAddr("192.168.1.1")
)
revExtIPv4, err := netutil.IPToReversedAddr(onesIP)
revExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())
require.NoError(t, err)
extUpstream := &aghtest.UpstreamMock{
@@ -1290,7 +1290,7 @@ func TestServer_Exchange(t *testing.T) {
},
}
revLocIPv4, err := netutil.IPToReversedAddr(localIP)
revLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
require.NoError(t, err)
locUpstream := &aghtest.UpstreamMock{
@@ -1330,7 +1330,7 @@ func TestServer_Exchange(t *testing.T) {
want string
wantErr error
locUpstream upstream.Upstream
req net.IP
req netip.Addr
}{{
name: "external_good",
want: onesHost,
@@ -1354,7 +1354,7 @@ func TestServer_Exchange(t *testing.T) {
want: "",
wantErr: ErrRDNSNoData,
locUpstream: locUpstream,
req: net.IP{192, 168, 1, 2},
req: netip.MustParseAddr("192.168.1.2"),
}, {
name: "invalid_answer",
want: "",
@@ -1396,3 +1396,57 @@ func TestServer_Exchange(t *testing.T) {
assert.Empty(t, host)
})
}
func TestServer_ShouldResolveClient(t *testing.T) {
srv := &Server{
privateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}
testCases := []struct {
ip netip.Addr
want require.BoolAssertionFunc
name string
resolve bool
usePrivate bool
}{{
name: "default",
ip: netip.MustParseAddr("1.1.1.1"),
want: require.True,
resolve: true,
usePrivate: true,
}, {
name: "no_rdns",
ip: netip.MustParseAddr("1.1.1.1"),
want: require.False,
resolve: false,
usePrivate: true,
}, {
name: "loopback",
ip: netip.MustParseAddr("127.0.0.1"),
want: require.False,
resolve: true,
usePrivate: true,
}, {
name: "private_resolve",
ip: netip.MustParseAddr("192.168.0.1"),
want: require.True,
resolve: true,
usePrivate: true,
}, {
name: "private_no_resolve",
ip: netip.MustParseAddr("192.168.0.1"),
want: require.False,
resolve: true,
usePrivate: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
srv.conf.ResolveClients = tc.resolve
srv.conf.UsePrivateRDNS = tc.usePrivate
ok := srv.ShouldResolveClient(tc.ip)
tc.want(t, ok)
})
}
}

View File

@@ -21,6 +21,8 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
||cname.specific^$dnstype=~CNAME
||0.0.0.1^$dnstype=~A
||::1^$dnstype=~AAAA
0.0.0.0 duplicate.domain
0.0.0.0 duplicate.domain
`
forwardConf := ServerConfig{
@@ -137,6 +139,17 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
},
A: netutil.IPv4Zero(),
}},
}, {
req: createTestMessage("duplicate.domain."),
name: "duplicate_domain",
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "duplicate.domain.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: netutil.IPv4Zero(),
}},
}}
for _, tc := range testCases {

View File

@@ -633,61 +633,70 @@ func (err domainSpecificTestError) Error() (msg string) {
return fmt.Sprintf("WARNING: %s", err.error)
}
// checkDNS checks the upstream server defined by upstreamConfigStr using
// healthCheck for actually exchange messages. It uses bootstrap to resolve the
// upstream's address.
func checkDNS(
upstreamConfigStr string,
bootstrap []string,
bootstrapPrefIPv6 bool,
timeout time.Duration,
healthCheck healthCheckFunc,
) (err error) {
if IsCommentOrEmpty(upstreamConfigStr) {
return nil
// parseUpstreamLine parses line and creates the [upstream.Upstream] using opts
// and information from [s.dnsFilter.EtcHosts]. It returns an error if the line
// is not a valid upstream line, see [upstream.AddressToUpstream]. It's a
// caller's responsibility to close u.
func (s *Server) parseUpstreamLine(
line string,
opts *upstream.Options,
) (u upstream.Upstream, specific bool, err error) {
// Separate upstream from domains list.
upstreamAddr, domains, err := separateUpstream(line)
if err != nil {
return nil, false, fmt.Errorf("wrong upstream format: %w", err)
}
// Separate upstream from domains list.
upstreamAddr, domains, err := separateUpstream(upstreamConfigStr)
if err != nil {
return fmt.Errorf("wrong upstream format: %w", err)
}
specific = len(domains) > 0
useDefault, err := validateUpstream(upstreamAddr, domains)
if err != nil {
return fmt.Errorf("wrong upstream format: %w", err)
return nil, specific, fmt.Errorf("wrong upstream format: %w", err)
} else if useDefault {
return nil
}
if len(bootstrap) == 0 {
bootstrap = defaultBootstrap
return nil, specific, nil
}
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
u, err := upstream.AddressToUpstream(upstreamAddr, &upstream.Options{
Bootstrap: bootstrap,
Timeout: timeout,
PreferIPv6: bootstrapPrefIPv6,
})
opts = &upstream.Options{
Bootstrap: opts.Bootstrap,
Timeout: opts.Timeout,
PreferIPv6: opts.PreferIPv6,
}
if s.dnsFilter != nil && s.dnsFilter.EtcHosts != nil {
resolved := s.resolveUpstreamHost(extractUpstreamHost(upstreamAddr))
sortNetIPAddrs(resolved, opts.PreferIPv6)
opts.ServerIPAddrs = resolved
}
u, err = upstream.AddressToUpstream(upstreamAddr, opts)
if err != nil {
return fmt.Errorf("failed to choose upstream for %q: %w", upstreamAddr, err)
return nil, specific, fmt.Errorf("creating upstream for %q: %w", upstreamAddr, err)
}
return u, specific, nil
}
func (s *Server) checkDNS(line string, opts *upstream.Options, check healthCheckFunc) (err error) {
if IsCommentOrEmpty(line) {
return nil
}
var u upstream.Upstream
var specific bool
defer func() {
if err != nil && specific {
err = domainSpecificTestError{error: err}
}
}()
u, specific, err = s.parseUpstreamLine(line, opts)
if err != nil || u == nil {
return err
}
defer func() { err = errors.WithDeferred(err, u.Close()) }()
if err = healthCheck(u); err != nil {
err = fmt.Errorf("upstream %q fails to exchange: %w", upstreamAddr, err)
if domains != nil {
return domainSpecificTestError{error: err}
}
return err
}
log.Debug("dnsforward: upstream %q is ok", upstreamAddr)
return nil
return check(u)
}
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
@@ -699,47 +708,54 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
return
}
result := map[string]string{}
bootstraps := req.BootstrapDNS
bootstrapPrefIPv6 := s.conf.BootstrapPreferIPv6
timeout := s.conf.UpstreamTimeout
opts := &upstream.Options{
Bootstrap: req.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
PreferIPv6: s.conf.BootstrapPreferIPv6,
}
if len(opts.Bootstrap) == 0 {
opts.Bootstrap = defaultBootstrap
}
type upsCheckResult = struct {
res string
err error
host string
}
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
upsNum := len(req.Upstreams) + len(req.PrivateUpstreams)
result := make(map[string]string, upsNum)
resCh := make(chan upsCheckResult, upsNum)
checkUps := func(ups string, healthCheck healthCheckFunc) {
res := upsCheckResult{
host: ups,
}
defer func() { resCh <- res }()
checkErr := checkDNS(ups, bootstraps, bootstrapPrefIPv6, timeout, healthCheck)
if checkErr != nil {
res.res = checkErr.Error()
} else {
res.res = "OK"
}
}
for _, ups := range req.Upstreams {
go checkUps(ups, checkDNSUpstreamExc)
go func(ups string) {
resCh <- upsCheckResult{
host: ups,
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
}
}(ups)
}
for _, ups := range req.PrivateUpstreams {
go checkUps(ups, checkPrivateUpstreamExc)
go func(ups string) {
resCh <- upsCheckResult{
host: ups,
err: s.checkDNS(ups, opts, checkPrivateUpstreamExc),
}
}(ups)
}
for i := 0; i < upsNum; i++ {
pair := <-resCh
// TODO(e.burkov): The upstreams used for both common and private
// resolving should be reported separately.
result[pair.host] = pair.res
pair := <-resCh
if pair.err != nil {
result[pair.host] = pair.err.Error()
} else {
result[pair.host] = "OK"
}
}
close(resCh)
_ = aghhttp.WriteJSONResponse(w, r, result)
}

View File

@@ -13,10 +13,12 @@ import (
"path/filepath"
"strings"
"testing"
"testing/fstest"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/netutil"
@@ -280,6 +282,10 @@ func TestIsCommentOrEmpty(t *testing.T) {
}
func TestValidateUpstreams(t *testing.T) {
const sdnsStamp = `sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_J` +
`S3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczE` +
`uYWRndWFyZC5jb20`
testCases := []struct {
name string
wantErr string
@@ -300,7 +306,7 @@ func TestValidateUpstreams(t *testing.T) {
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
"[/host/]" + sdnsStamp,
},
}, {
name: "with_default",
@@ -310,7 +316,7 @@ func TestValidateUpstreams(t *testing.T) {
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
"[/host/]" + sdnsStamp,
"8.8.8.8",
},
}, {
@@ -326,9 +332,10 @@ func TestValidateUpstreams(t *testing.T) {
wantErr: `validating upstream "123.3.7m": not an ip:port`,
set: []string{"123.3.7m"},
}, {
name: "invalid",
wantErr: `bad upstream for domain "[/host.com]tls://dns.adguard.com": missing separator`,
set: []string{"[/host.com]tls://dns.adguard.com"},
name: "invalid",
wantErr: `bad upstream for domain "[/host.com]tls://dns.adguard.com": ` +
`missing separator`,
set: []string{"[/host.com]tls://dns.adguard.com"},
}, {
name: "invalid",
wantErr: `validating upstream "[host.ru]#": not an ip:port`,
@@ -340,14 +347,14 @@ func TestValidateUpstreams(t *testing.T) {
"1.1.1.1",
"tls://1.1.1.1",
"https://dns.adguard.com/dns-query",
"sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
sdnsStamp,
"udp://dns.google",
"udp://8.8.8.8",
"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
"[/host/]" + sdnsStamp,
"[/пример.рф/]8.8.8.8",
},
}, {
@@ -418,27 +425,28 @@ func TestValidateUpstreamsPrivate(t *testing.T) {
}
}
func newLocalUpstreamListener(t *testing.T, port int, handler dns.Handler) (real net.Addr) {
func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (real netip.AddrPort) {
t.Helper()
startCh := make(chan struct{})
upsSrv := &dns.Server{
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(port)).String(),
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), port).String(),
Net: "tcp",
Handler: handler,
NotifyStartedFunc: func() { close(startCh) },
}
go func() {
t := testutil.PanicT{}
err := upsSrv.ListenAndServe()
require.NoError(t, err)
require.NoError(testutil.PanicT{}, err)
}()
<-startCh
testutil.CleanupAndRequireSuccess(t, upsSrv.Shutdown)
return upsSrv.Listener.Addr()
return testutil.RequireTypeAssert[*net.TCPAddr](t, upsSrv.Listener.Addr()).AddrPort()
}
func TestServer_handleTestUpstreaDNS(t *testing.T) {
func TestServer_HandleTestUpstreamDNS(t *testing.T) {
goodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
err := w.WriteMsg(new(dns.Msg).SetReply(m))
require.NoError(testutil.PanicT{}, err)
@@ -457,9 +465,38 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
Host: newLocalUpstreamListener(t, 0, badHandler).String(),
}).String()
const upsTimeout = 100 * time.Millisecond
const (
upsTimeout = 100 * time.Millisecond
srv := createTestServer(t, &filtering.Config{}, ServerConfig{
hostsFileName = "hosts"
upstreamHost = "custom.localhost"
)
hostsListener := newLocalUpstreamListener(t, 0, goodHandler)
hostsUps := (&url.URL{
Scheme: "tcp",
Host: netutil.JoinHostPort(upstreamHost, int(hostsListener.Port())),
}).String()
hc, err := aghnet.NewHostsContainer(
filtering.SysHostsListID,
fstest.MapFS{
hostsFileName: &fstest.MapFile{
Data: []byte(hostsListener.Addr().String() + " " + upstreamHost),
},
},
&aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { return nil },
OnAdd: func(_ string) (err error) { return nil },
OnClose: func() (err error) { return nil },
},
hostsFileName,
)
require.NoError(t, err)
srv := createTestServer(t, &filtering.Config{
EtcHosts: hc,
}, ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}},
UpstreamTimeout: upsTimeout,
@@ -486,8 +523,7 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
"upstream_dns": []string{badUps},
},
wantResp: map[string]any{
badUps: `upstream "` + badUps + `" fails to exchange: ` +
`couldn't communicate with upstream: exchanging with ` +
badUps: `couldn't communicate with upstream: exchanging with ` +
badUps + ` over tcp: dns: id mismatch`,
},
name: "broken",
@@ -497,20 +533,40 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
},
wantResp: map[string]any{
goodUps: "OK",
badUps: `upstream "` + badUps + `" fails to exchange: ` +
`couldn't communicate with upstream: exchanging with ` +
badUps: `couldn't communicate with upstream: exchanging with ` +
badUps + ` over tcp: dns: id mismatch`,
},
name: "both",
}, {
body: map[string]any{
"upstream_dns": []string{"[/domain.example/]" + badUps},
},
wantResp: map[string]any{
"[/domain.example/]" + badUps: `WARNING: couldn't communicate ` +
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
`dns: id mismatch`,
},
name: "domain_specific_error",
}, {
body: map[string]any{
"upstream_dns": []string{hostsUps},
},
wantResp: map[string]any{
hostsUps: "OK",
},
name: "etc_hosts",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqBody, err := json.Marshal(tc.body)
var reqBody []byte
reqBody, err = json.Marshal(tc.body)
require.NoError(t, err)
w := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
require.NoError(t, err)
srv.handleTestUpstreamDNS(w, r)
@@ -538,11 +594,15 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
req := map[string]any{
"upstream_dns": []string{sleepyUps},
}
reqBody, err := json.Marshal(req)
var reqBody []byte
reqBody, err = json.Marshal(req)
require.NoError(t, err)
w := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
require.NoError(t, err)
srv.handleTestUpstreamDNS(w, r)

View File

@@ -26,11 +26,25 @@ func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) {
return resp
}
// ipsFromRules extracts non-IP addresses from the filtering result rules.
// containsIP returns true if the IP is already in the list.
func containsIP(ips []net.IP, ip net.IP) bool {
for _, a := range ips {
if a.Equal(ip) {
return true
}
}
return false
}
// ipsFromRules extracts unique non-IP addresses from the filtering result
// rules.
func ipsFromRules(resRules []*filtering.ResultRule) (ips []net.IP) {
for _, r := range resRules {
if r.IP != nil {
ips = append(ips, r.IP)
// len(resRules) and len(ips) are actually small enough for O(n^2) to do
// not raise performance questions.
if ip := r.IP; ip != nil && !containsIP(ips, ip) {
ips = append(ips, ip)
}
}

View File

@@ -0,0 +1,311 @@
package dnsforward
import (
"bytes"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
// loadUpstreams parses upstream DNS servers from the configured file or from
// the configuration itself.
func (s *Server) loadUpstreams() (upstreams []string, err error) {
if s.conf.UpstreamDNSFileName == "" {
return stringutil.FilterOut(s.conf.UpstreamDNS, IsCommentOrEmpty), nil
}
var data []byte
data, err = os.ReadFile(s.conf.UpstreamDNSFileName)
if err != nil {
return nil, fmt.Errorf("reading upstream from file: %w", err)
}
upstreams = stringutil.SplitTrimmed(string(data), "\n")
log.Debug("dnsforward: got %d upstreams in %q", len(upstreams), s.conf.UpstreamDNSFileName)
return stringutil.FilterOut(upstreams, IsCommentOrEmpty), nil
}
// prepareUpstreamSettings sets upstream DNS server settings.
func (s *Server) prepareUpstreamSettings() (err error) {
// We're setting a customized set of RootCAs. The reason is that Go default
// mechanism of loading TLS roots does not always work properly on some
// routers so we're loading roots manually and pass it here.
//
// See [aghtls.SystemRootCAs].
upstream.RootCAs = s.conf.TLSv12Roots
upstream.CipherSuites = s.conf.TLSCiphers
// Load upstreams either from the file, or from the settings
var upstreams []string
upstreams, err = s.loadUpstreams()
if err != nil {
return fmt.Errorf("loading upstreams: %w", err)
}
s.conf.UpstreamConfig, err = s.prepareUpstreamConfig(upstreams, defaultDNS, &upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
PreferIPv6: s.conf.BootstrapPreferIPv6,
})
if err != nil {
return fmt.Errorf("preparing upstream config: %w", err)
}
return nil
}
// prepareUpstreamConfig sets upstream configuration based on upstreams and
// configuration of s.
func (s *Server) prepareUpstreamConfig(
upstreams []string,
defaultUpstreams []string,
opts *upstream.Options,
) (uc *proxy.UpstreamConfig, err error) {
uc, err = proxy.ParseUpstreamsConfig(upstreams, opts)
if err != nil {
return nil, fmt.Errorf("parsing upstream config: %w", err)
}
if len(uc.Upstreams) == 0 && defaultUpstreams != nil {
log.Info("dnsforward: warning: no default upstreams specified, using %v", defaultUpstreams)
var defaultUpstreamConfig *proxy.UpstreamConfig
defaultUpstreamConfig, err = proxy.ParseUpstreamsConfig(defaultUpstreams, opts)
if err != nil {
return nil, fmt.Errorf("parsing default upstreams: %w", err)
}
uc.Upstreams = defaultUpstreamConfig.Upstreams
}
if s.dnsFilter != nil && s.dnsFilter.EtcHosts != nil {
err = s.replaceUpstreamsWithHosts(uc, opts)
if err != nil {
return nil, fmt.Errorf("resolving upstreams with hosts: %w", err)
}
}
return uc, nil
}
// replaceUpstreamsWithHosts replaces unique upstreams with their resolved
// versions based on the system hosts file.
//
// TODO(e.burkov): This should be performed inside dnsproxy, which should
// actually consider /etc/hosts. See TODO on [aghnet.HostsContainer].
func (s *Server) replaceUpstreamsWithHosts(
upsConf *proxy.UpstreamConfig,
opts *upstream.Options,
) (err error) {
resolved := map[string]*upstream.Options{}
err = s.resolveUpstreamsWithHosts(resolved, upsConf.Upstreams, opts)
if err != nil {
return fmt.Errorf("resolving upstreams: %w", err)
}
hosts := maps.Keys(upsConf.DomainReservedUpstreams)
// TODO(e.burkov): Think of extracting sorted range into an util function.
slices.Sort(hosts)
for _, host := range hosts {
err = s.resolveUpstreamsWithHosts(resolved, upsConf.DomainReservedUpstreams[host], opts)
if err != nil {
return fmt.Errorf("resolving upstreams reserved for %s: %w", host, err)
}
}
hosts = maps.Keys(upsConf.SpecifiedDomainUpstreams)
slices.Sort(hosts)
for _, host := range hosts {
err = s.resolveUpstreamsWithHosts(resolved, upsConf.SpecifiedDomainUpstreams[host], opts)
if err != nil {
return fmt.Errorf("resolving upstreams specific for %s: %w", host, err)
}
}
return nil
}
// resolveUpstreamsWithHosts resolves the IP addresses of each of the upstreams
// and replaces those both in upstreams and resolved. Upstreams that failed to
// resolve are placed to resolved as-is. This function only returns error of
// upstreams closing.
func (s *Server) resolveUpstreamsWithHosts(
resolved map[string]*upstream.Options,
upstreams []upstream.Upstream,
opts *upstream.Options,
) (err error) {
for i := range upstreams {
u := upstreams[i]
addr := u.Address()
host := extractUpstreamHost(addr)
withIPs, ok := resolved[host]
if !ok {
ips := s.resolveUpstreamHost(host)
if len(ips) == 0 {
resolved[host] = nil
return nil
}
sortNetIPAddrs(ips, opts.PreferIPv6)
withIPs = opts.Clone()
withIPs.ServerIPAddrs = ips
resolved[host] = withIPs
} else if withIPs == nil {
continue
}
if err = u.Close(); err != nil {
return fmt.Errorf("closing upstream %s: %w", addr, err)
}
upstreams[i], err = upstream.AddressToUpstream(addr, withIPs)
if err != nil {
return fmt.Errorf("replacing upstream %s with resolved %s: %w", addr, host, err)
}
log.Debug("dnsforward: using %s for %s", withIPs.ServerIPAddrs, upstreams[i].Address())
}
return nil
}
// extractUpstreamHost returns the hostname of addr without port with an
// assumption that any address passed here has already been successfully parsed
// by [upstream.AddressToUpstream]. This function eesentially mirrors the logic
// of [upstream.AddressToUpstream], see TODO on [replaceUpstreamsWithHosts].
func extractUpstreamHost(addr string) (host string) {
var err error
if strings.Contains(addr, "://") {
var u *url.URL
u, err = url.Parse(addr)
if err != nil {
log.Debug("dnsforward: parsing upstream %s: %s", addr, err)
return addr
}
return u.Hostname()
}
// Probably, plain UDP upstream defined by address or address:port.
host, err = netutil.SplitHost(addr)
if err != nil {
return addr
}
return host
}
// resolveUpstreamHost returns the version of ups with IP addresses from the
// system hosts file placed into its options.
func (s *Server) resolveUpstreamHost(host string) (addrs []net.IP) {
req := &urlfilter.DNSRequest{
Hostname: host,
DNSType: dns.TypeA,
}
aRes, _ := s.dnsFilter.EtcHosts.MatchRequest(req)
req.DNSType = dns.TypeAAAA
aaaaRes, _ := s.dnsFilter.EtcHosts.MatchRequest(req)
var ips []net.IP
for _, rw := range append(aRes.DNSRewrites(), aaaaRes.DNSRewrites()...) {
dr := rw.DNSRewrite
if dr == nil || dr.Value == nil {
continue
}
if ip, ok := dr.Value.(net.IP); ok {
ips = append(ips, ip)
}
}
return ips
}
// sortNetIPAddrs sorts addrs in accordance with the protocol preferences.
// Invalid addresses are sorted near the end.
//
// TODO(e.burkov): This function taken from dnsproxy, which also already
// contains a few similar functions. Think of moving to golibs.
func sortNetIPAddrs(addrs []net.IP, preferIPv6 bool) {
l := len(addrs)
if l <= 1 {
return
}
slices.SortStableFunc(addrs, func(addrA, addrB net.IP) (sortsBefore bool) {
switch len(addrA) {
case net.IPv4len, net.IPv6len:
switch len(addrB) {
case net.IPv4len, net.IPv6len:
// Go on.
default:
return true
}
default:
return false
}
if aIs4, bIs4 := addrA.To4() != nil, addrB.To4() != nil; aIs4 != bIs4 {
if aIs4 {
return !preferIPv6
}
return preferIPv6
}
return bytes.Compare(addrA, addrB) < 0
})
}
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// setProxyUpstreamMode sets the upstream mode and related settings in conf
// based on provided parameters.
func setProxyUpstreamMode(
conf *proxy.Config,
allServers bool,
fastestAddr bool,
fastestTimeout time.Duration,
) {
if allServers {
conf.UpstreamMode = proxy.UModeParallel
} else if fastestAddr {
conf.UpstreamMode = proxy.UModeFastestAddr
conf.FastestPingTimeout = fastestTimeout
} else {
conf.UpstreamMode = proxy.UModeLoadBalance
}
}

View File

@@ -1,10 +1,7 @@
package filtering
import (
"bufio"
"bytes"
"fmt"
"hash/crc32"
"io"
"net/http"
"os"
@@ -14,6 +11,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
@@ -29,9 +27,9 @@ const filterDir = "filters"
// TODO(e.burkov): Use more deterministic approach.
var nextFilterID = time.Now().Unix()
// FilterYAML respresents a filter list in the configuration file.
// FilterYAML represents a filter list in the configuration file.
//
// TODO(e.burkov): Investigate if the field oredering is important.
// TODO(e.burkov): Investigate if the field ordering is important.
type FilterYAML struct {
Enabled bool
URL string // URL or a file path
@@ -213,7 +211,7 @@ func (d *DNSFilter) loadFilters(array []FilterYAML) {
err := d.load(filter)
if err != nil {
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
log.Error("filtering: loading filter %d: %s", filter.ID, err)
}
}
}
@@ -338,7 +336,8 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
updateFlags = append(updateFlags, updated)
if err != nil {
nfail++
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
log.Info("filtering: updating filter from url %q: %s\n", uf.URL, err)
continue
}
}
@@ -367,7 +366,13 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
continue
}
log.Info("Updated filter #%d. Rules: %d -> %d", f.ID, f.RulesCount, uf.RulesCount)
log.Info(
"filtering: updated filter %d; rule count: %d (was %d)",
f.ID,
uf.RulesCount,
f.RulesCount,
)
f.Name = uf.Name
f.RulesCount = uf.RulesCount
f.checksum = uf.checksum
@@ -397,9 +402,10 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
//
// TODO(a.garipov, e.burkov): What the hell?
func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
log.Debug("filtering: updating...")
updNum := 0
log.Debug("filtering: starting updating")
defer func() { log.Debug("filtering: finished updating, %d updated", updNum) }()
var lists []FilterYAML
var toUpd []bool
isNetErr := false
@@ -437,131 +443,9 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
}
}
log.Debug("filtering: update finished: %d lists updated", updNum)
return updNum, false
}
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
// characters.
//
// TODO(e.burkov): Investigate the purpose of this and improve the
// implementation. Perhaps, use something from the unicode package.
func isPrintableText(data string) (ok bool) {
for _, c := range []byte(data) {
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
continue
}
return false
}
return true
}
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
// line breaks.
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, data[0 : i+1], nil
}
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// parseFilter copies filter's content from src to dst and returns the number of
// rules, number of bytes written, checksum, and title of the parsed list. dst
// must not be nil.
func (d *DNSFilter) parseFilter(
src io.Reader,
dst io.Writer,
) (rulesNum, written int, checksum uint32, title string, err error) {
scanner := bufio.NewScanner(src)
scanner.Split(scanLinesWithBreak)
titleFound := false
for n := 0; scanner.Scan(); written += n {
line := scanner.Text()
var isRule bool
var likelyTitle string
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
if err != nil {
return 0, written, 0, "", err
}
if isRule {
rulesNum++
} else if likelyTitle != "" {
title, titleFound = likelyTitle, true
}
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
n, err = dst.Write([]byte(line))
if err != nil {
return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
}
}
if err = scanner.Err(); err != nil {
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
}
return rulesNum, written, checksum, title, nil
}
// parseFilterLine returns true if the passed line is a rule. line is
// considered a rule if it's not a comment and contains no title.
func (d *DNSFilter) parseFilterLine(
line string,
lookForTitle bool,
testHTML bool,
) (isRule bool, title string, err error) {
if !isPrintableText(line) {
return false, "", errors.Error("filter contains non-printable characters")
}
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
return false, "", nil
}
if testHTML && isHTML(line) {
return false, "", errors.Error("data is HTML, not plain text")
}
if line[0] == '!' && lookForTitle {
match := d.filterTitleRegexp.FindStringSubmatch(line)
if len(match) > 1 {
title = match[1]
}
return false, title, nil
}
return true, "", nil
}
// isHTML returns true if the line contains HTML tags instead of plain text.
// line shouldn have no leading space symbols.
//
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
// check if trimmed string begins with angle bracket.
func isHTML(line string) (ok bool) {
line = strings.ToLower(line)
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
}
// update refreshes filter's content and a/mtimes of it's file.
func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
b, err = d.updateIntl(filter)
@@ -573,7 +457,7 @@ func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
filter.LastUpdated,
)
if chErr != nil {
log.Error("os.Chtimes(): %v", chErr)
log.Error("filtering: os.Chtimes(): %s", chErr)
}
}
@@ -582,14 +466,12 @@ func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
// finalizeUpdate closes and gets rid of temporary file f with filter's content
// according to updated. It also saves new values of flt's name, rules number
// and checksum if sucсeeded.
// and checksum if succeeded.
func (d *DNSFilter) finalizeUpdate(
file *os.File,
flt *FilterYAML,
updated bool,
name string,
rnum int,
cs uint32,
res *rulelist.ParseResult,
) (err error) {
tmpFileName := file.Name()
@@ -602,23 +484,24 @@ func (d *DNSFilter) finalizeUpdate(
}
if !updated {
log.Tracef("filter #%d from %s has no changes, skip", flt.ID, flt.URL)
log.Debug("filtering: filter %d from url %q has no changes, skipping", flt.ID, flt.URL)
return os.Remove(tmpFileName)
}
fltPath := flt.Path(d.DataDir)
log.Printf("saving contents of filter #%d into %s", flt.ID, fltPath)
log.Info("filtering: saving contents of filter %d into %q", flt.ID, fltPath)
// Don't use renamio or maybe packages, since those will require loading the
// whole filter content to the memory on Windows.
// Don't use renameio or maybe packages, since those will require loading
// the whole filter content to the memory on Windows.
err = os.Rename(tmpFileName, fltPath)
if err != nil {
return errors.WithDeferred(err, os.Remove(tmpFileName))
}
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
flt.Name = aghalg.Coalesce(flt.Name, res.Title)
flt.checksum, flt.RulesCount = res.Checksum, res.RulesCount
return nil
}
@@ -626,11 +509,9 @@ func (d *DNSFilter) finalizeUpdate(
// updateIntl updates the flt rewriting it's actual file. It returns true if
// the actual update has been performed.
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
log.Tracef("downloading update for filter %d from %s", flt.ID, flt.URL)
log.Debug("filtering: downloading update for filter %d from %q", flt.ID, flt.URL)
var name string
var rnum, n int
var cs uint32
var res *rulelist.ParseResult
var tmpFile *os.File
tmpFile, err = os.CreateTemp(filepath.Join(d.DataDir, filterDir), "")
@@ -638,9 +519,14 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
return false, err
}
defer func() {
finErr := d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs)
finErr := d.finalizeUpdate(tmpFile, flt, ok, res)
if ok && finErr == nil {
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
log.Info(
"filtering: updated filter %d: %d bytes, %d rules",
flt.ID,
res.BytesWritten,
res.RulesCount,
)
return
}
@@ -661,14 +547,14 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
var resp *http.Response
resp, err = d.HTTPClient.Get(flt.URL)
if err != nil {
log.Printf("requesting filter from %s, skip: %s", flt.URL, err)
log.Info("filtering: requesting filter from %q: %s, skipping", flt.URL, err)
return false, err
}
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
log.Info("filtering got status code %d from %q, skipping", resp.StatusCode, flt.URL)
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
}
@@ -685,16 +571,20 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
r = f
}
rnum, n, cs, name, err = d.parseFilter(r, tmpFile)
bufPtr := d.bufPool.Get().(*[]byte)
defer d.bufPool.Put(bufPtr)
return cs != flt.checksum && err == nil, err
p := rulelist.NewParser()
res, err = p.Parse(tmpFile, r, *bufPtr)
return res.Checksum != flt.checksum && err == nil, err
}
// loads filter contents from the file in dataDir
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
fileName := flt.Path(d.DataDir)
log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
log.Debug("filtering: loading filter %d from %q", flt.ID, fileName)
file, err := os.Open(fileName)
if errors.Is(err, os.ErrNotExist) {
@@ -710,14 +600,18 @@ func (d *DNSFilter) load(flt *FilterYAML) (err error) {
return fmt.Errorf("getting filter file stat: %w", err)
}
log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
log.Debug("filtering: file %q, id %d, length %d", fileName, flt.ID, st.Size())
rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
bufPtr := d.bufPool.Get().(*[]byte)
defer d.bufPool.Put(bufPtr)
p := rulelist.NewParser()
res, err := p.Parse(io.Discard, file, *bufPtr)
if err != nil {
return fmt.Errorf("parsing filter file: %w", err)
}
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
flt.RulesCount, flt.checksum, flt.LastUpdated = res.RulesCount, res.Checksum, st.ModTime()
return nil
}
@@ -759,8 +653,9 @@ func (d *DNSFilter) enableFiltersLocked(async bool) {
})
}
if err := d.SetFilters(filters, allowFilters, async); err != nil {
log.Debug("enabling filters: %s", err)
err := d.setFilters(filters, allowFilters, async)
if err != nil {
log.Error("filtering: enabling filters: %s", err)
}
d.SetEnabled(d.FilteringEnabled)

View File

@@ -9,7 +9,6 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strings"
@@ -18,6 +17,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
@@ -170,6 +170,15 @@ type Checker interface {
// DNSFilter matches hostnames and DNS requests against filtering rules.
type DNSFilter struct {
// bufPool is a pool of buffers used for filtering-rule list parsing.
bufPool *sync.Pool
rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine
rulesStorageAllow *filterlist.RuleStorage
filteringEngineAllow *urlfilter.DNSEngine
safeSearch SafeSearch
// safeBrowsingChecker is the safe browsing hash-prefix checker.
@@ -178,12 +187,6 @@ type DNSFilter struct {
// parentalControl is the parental control hash-prefix checker.
parentalControlChecker Checker
rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine
rulesStorageAllow *filterlist.RuleStorage
filteringEngineAllow *urlfilter.DNSEngine
engineLock sync.RWMutex
Config // for direct access by library users, even a = assignment
@@ -196,12 +199,6 @@ type DNSFilter struct {
refreshLock *sync.Mutex
// filterTitleRegexp is the regular expression to retrieve a name of a
// filter list.
//
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
filterTitleRegexp *regexp.Regexp
hostCheckers []hostChecker
}
@@ -339,12 +336,12 @@ func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
return clone
}
// SetFilters sets new filters, synchronously or asynchronously. When filters
// setFilters sets new filters, synchronously or asynchronously. When filters
// are set asynchronously, the old filters continue working until the new
// filters are ready.
//
// In this case the caller must ensure that the old filter files are intact.
func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error {
func (d *DNSFilter) setFilters(blockFilters, allowFilters []Filter, async bool) error {
if async {
params := filtersInitializerParams{
allowFilters: allowFilters,
@@ -370,14 +367,7 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
return nil
}
err := d.initFiltering(allowFilters, blockFilters)
if err != nil {
log.Error("filtering: can't initialize filtering subsystem: %s", err)
return err
}
return nil
return d.initFiltering(allowFilters, blockFilters)
}
// Starts initializing new filters by signal from channel
@@ -386,7 +376,8 @@ func (d *DNSFilter) filtersInitializer() {
params := <-d.filtersInitializerChan
err := d.initFiltering(params.allowFilters, params.blockFilters)
if err != nil {
log.Error("Can't initialize filtering subsystem: %s", err)
log.Error("filtering: initializing: %s", err)
continue
}
}
@@ -519,7 +510,7 @@ func (d *DNSFilter) matchSysHosts(
dnsres, _ := d.EtcHosts.MatchRequest(&urlfilter.DNSRequest{
Hostname: host,
SortedClientTags: setts.ClientTags,
// TODO(e.burkov): Wait for urlfilter update to pass net.IP.
// TODO(e.burkov): Wait for urlfilter update to pass netip.Addr.
ClientIP: setts.ClientIP.String(),
ClientName: setts.ClientName,
DNSType: qtype,
@@ -718,7 +709,7 @@ func newRuleStorage(filters []Filter) (rs *filterlist.RuleStorage, err error) {
}
// Initialize urlfilter objects.
func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) (err error) {
rulesStorage, err := newRuleStorage(blockFilters)
if err != nil {
return err
@@ -745,7 +736,8 @@ func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
// Make sure that the OS reclaims memory as soon as possible.
debug.FreeOSMemory()
log.Debug("initialized filtering engine")
log.Debug("filtering: initialized filtering engine")
return nil
}
@@ -949,8 +941,14 @@ func InitModule() {
// be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{
bufPool: &sync.Pool{
New: func() (buf any) {
bufVal := make([]byte, rulelist.MaxRuleLen)
return &bufVal
},
},
refreshLock: &sync.Mutex{},
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
safeBrowsingChecker: c.SafeBrowsingChecker,
parentalControlChecker: c.ParentalControlChecker,
}
@@ -1047,7 +1045,7 @@ func (d *DNSFilter) checkSafeBrowsing(
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("safebrowsing lookup for %q", host)
defer timer.LogElapsed("filtering: safebrowsing lookup for %q", host)
}
res = Result{
@@ -1079,7 +1077,7 @@ func (d *DNSFilter) checkParental(
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("parental lookup for %q", host)
defer timer.LogElapsed("filtering: parental lookup for %q", host)
}
res = Result{

View File

@@ -547,7 +547,7 @@ func TestWhitelist(t *testing.T) {
}}
d, setts := newForTest(t, nil, filters)
err := d.SetFilters(filters, whiteFilters, false)
err := d.setFilters(filters, whiteFilters, false)
require.NoError(t, err)
t.Cleanup(d.Close)

View File

@@ -25,7 +25,7 @@ func toCacheItem(data []byte) *cacheItem {
t := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
data = data[expirySize:]
hashes := make([]hostnameHash, len(data)/hashSize)
hashes := make([]hostnameHash, 0, len(data)/hashSize)
for i := 0; i < len(data); i += hashSize {
var hash hostnameHash
@@ -41,12 +41,13 @@ func toCacheItem(data []byte) *cacheItem {
// fromCacheItem encodes cacheItem into data.
func fromCacheItem(item *cacheItem) (data []byte) {
data = make([]byte, len(item.hashes)*hashSize+expirySize)
data = make([]byte, 0, len(item.hashes)*hashSize+expirySize)
expiry := item.expiry.Unix()
binary.BigEndian.PutUint64(data[:expirySize], uint64(expiry))
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
for _, v := range item.hashes {
// nolint:looppointer // The subsilce is used for a copy.
// nolint:looppointer // The subslice of v is used for a copy.
data = append(data, v[:]...)
}
@@ -62,7 +63,7 @@ func (c *Checker) findInCache(
i := 0
for _, hash := range hashes {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
// nolint:looppointer // The has subslice is used for a cache lookup.
data := c.cache.Get(hash[:prefixLen])
if data == nil {
hashes[i] = hash
@@ -97,34 +98,36 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
for _, hash := range respHashes {
var pref prefix
// nolint:looppointer // The subsilce is used for a copy.
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:])
hashToStore[pref] = append(hashToStore[pref], hash)
}
for pref, hash := range hashToStore {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
c.setCache(pref[:], hash)
c.setCache(pref, hash)
}
for _, hash := range hashesToRequest {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
pref := hash[:prefixLen]
val := c.cache.Get(pref)
// nolint:looppointer // The hash subslice is used for a cache lookup.
val := c.cache.Get(hash[:prefixLen])
if val == nil {
var pref prefix
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:])
c.setCache(pref, nil)
}
}
}
// setCache stores hash in cache.
func (c *Checker) setCache(pref []byte, hashes []hostnameHash) {
func (c *Checker) setCache(pref prefix, hashes []hostnameHash) {
item := &cacheItem{
expiry: time.Now().Add(c.cacheTime),
hashes: hashes,
}
c.cache.Set(pref, fromCacheItem(item))
c.cache.Set(pref[:], fromCacheItem(item))
log.Debug("%s: stored in cache: %v", c.svc, pref)
}

View File

@@ -0,0 +1,44 @@
package hashprefix
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCacheItem(t *testing.T) {
item := &cacheItem{
expiry: time.Unix(0x01_23_45_67_89_AB_CD_EF, 0),
hashes: []hostnameHash{{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
}, {
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
}},
}
wantData := []byte{
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
}
gotData := fromCacheItem(item)
assert.Equal(t, wantData, gotData)
newItem := toCacheItem(gotData)
gotData = fromCacheItem(newItem)
assert.Equal(t, wantData, gotData)
}

View File

@@ -173,7 +173,7 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
b := &strings.Builder{}
for _, hash := range hashes {
// nolint:looppointer // The subsilce is used for safe hex encoding.
// nolint:looppointer // The hash subslice is used for hex encoding.
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
}

View File

@@ -95,7 +95,7 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
r,
w,
http.StatusBadRequest,
"Couldn't fetch filter from url %s: %s",
"Couldn't fetch filter from URL %q: %s",
filt.URL,
err,
)

View File

@@ -122,7 +122,7 @@ func matchDomainWildcard(host, wildcard string) (ok bool) {
return isWildcard(wildcard) && strings.HasSuffix(host, wildcard[1:])
}
// legacyRewriteSortsBefore sorts rewirtes according to the following priority:
// legacyRewriteSortsBefore sorts rewrites according to the following priority:
//
// 1. A and AAAA > CNAME;
// 2. wildcard > exact;

View File

@@ -0,0 +1,9 @@
package rulelist
import "github.com/AdguardTeam/golibs/errors"
// ErrHTML is returned by [Parser.Parse] if the data is likely to be HTML.
//
// TODO(a.garipov): This error is currently returned to the UI. Stop that and
// make it all-lowercase.
const ErrHTML errors.Error = "data is HTML, not plain text"

View File

@@ -0,0 +1,184 @@
package rulelist
import (
"bufio"
"bytes"
"fmt"
"hash/crc32"
"io"
"unicode"
"github.com/AdguardTeam/golibs/errors"
)
// Parser is a filtering-rule parser that collects data, such as the checksum
// and the title, as well as counts rules and removes comments.
type Parser struct {
title string
rulesCount int
written int
checksum uint32
titleFound bool
}
// NewParser returns a new filtering-rule parser.
func NewParser() (p *Parser) {
return &Parser{}
}
// ParseResult contains information about the results of parsing a
// filtering-rule list by [Parser.Parse].
type ParseResult struct {
// Title is the title contained within the filtering-rule list, if any.
Title string
// RulesCount is the number of rules in the list. It excludes empty lines
// and comments.
RulesCount int
// BytesWritten is the number of bytes written to dst.
BytesWritten int
// Checksum is the CRC-32 checksum of the rules content. That is, excluding
// empty lines and comments.
Checksum uint32
}
// Parse parses data from src into dst using buf during parsing. r is never
// nil.
func (p *Parser) Parse(dst io.Writer, src io.Reader, buf []byte) (r *ParseResult, err error) {
s := bufio.NewScanner(src)
s.Buffer(buf, MaxRuleLen)
lineIdx := 0
for s.Scan() {
var n int
n, err = p.processLine(dst, s.Bytes(), lineIdx)
p.written += n
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return p.result(), err
}
lineIdx++
}
r = p.result()
err = s.Err()
return r, errors.Annotate(err, "scanning filter contents: %w")
}
// result returns the current parsing result.
func (p *Parser) result() (r *ParseResult) {
return &ParseResult{
Title: p.title,
RulesCount: p.rulesCount,
BytesWritten: p.written,
Checksum: p.checksum,
}
}
// processLine processes a single line. It may write to dst, and if it does, n
// is the number of bytes written.
func (p *Parser) processLine(dst io.Writer, line []byte, lineIdx int) (n int, err error) {
trimmed := bytes.TrimSpace(line)
if p.written == 0 && isHTMLLine(trimmed) {
return 0, ErrHTML
}
badIdx, isRule := 0, false
if p.titleFound {
badIdx, isRule = parseLine(trimmed)
} else {
badIdx, isRule = p.parseLineTitle(trimmed)
}
if badIdx != -1 {
return 0, fmt.Errorf(
"line at index %d: character at index %d: non-printable character",
lineIdx,
badIdx+bytes.Index(line, trimmed),
)
}
if !isRule {
return 0, nil
}
p.rulesCount++
p.checksum = crc32.Update(p.checksum, crc32.IEEETable, trimmed)
// Assume that there is generally enough space in the buffer to add a
// newline.
n, err = dst.Write(append(trimmed, '\n'))
return n, errors.Annotate(err, "writing rule line: %w")
}
// isHTMLLine returns true if line is likely an HTML line. line is assumed to
// be trimmed of whitespace characters.
func isHTMLLine(line []byte) (isHTML bool) {
return hasPrefixFold(line, []byte("<html")) || hasPrefixFold(line, []byte("<!doctype"))
}
// hasPrefixFold is a simple, best-effort prefix matcher. It may return
// incorrect results for some non-ASCII characters.
func hasPrefixFold(b, prefix []byte) (ok bool) {
l := len(prefix)
return len(b) >= l && bytes.EqualFold(b[:l], prefix)
}
// parseLine returns true if the parsed line is a filtering rule. line is
// assumed to be trimmed of whitespace characters. nonPrintIdx is the index of
// the first non-printable character, if any; if there are none, nonPrintIdx is
// -1.
//
// A line is considered a rule if it's not empty, not a comment, and contains
// only printable characters.
func parseLine(line []byte) (nonPrintIdx int, isRule bool) {
if len(line) == 0 || line[0] == '#' || line[0] == '!' {
return -1, false
}
nonPrintIdx = bytes.IndexFunc(line, isNotPrintable)
return nonPrintIdx, nonPrintIdx == -1
}
// isNotPrintable returns true if r is not a printable character that can be
// contained in a filtering rule.
func isNotPrintable(r rune) (ok bool) {
// Tab isn't included into Unicode's graphic symbols, so include it here
// explicitly.
return r != '\t' && !unicode.IsGraphic(r)
}
// parseLineTitle is like [parseLine] but additionally looks for a title. line
// is assumed to be trimmed of whitespace characters.
func (p *Parser) parseLineTitle(line []byte) (nonPrintIdx int, isRule bool) {
if len(line) == 0 || line[0] == '#' {
return -1, false
}
if line[0] != '!' {
nonPrintIdx = bytes.IndexFunc(line, isNotPrintable)
return nonPrintIdx, nonPrintIdx == -1
}
const titlePattern = "! Title: "
if !bytes.HasPrefix(line, []byte(titlePattern)) {
return -1, false
}
title := bytes.TrimSpace(line[len(titlePattern):])
if title != nil {
// Note that title can be a non-nil empty slice. Consider that normal
// and just stop looking for other titles.
p.title = string(title)
p.titleFound = true
}
return -1, false
}

View File

@@ -0,0 +1,247 @@
package rulelist_test
import (
"bufio"
"bytes"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParser_Parse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
wantDst string
wantErrMsg string
wantTitle string
wantRulesNum int
wantWritten int
}{{
name: "empty",
in: "",
wantDst: "",
wantErrMsg: "",
wantTitle: "",
wantRulesNum: 0,
wantWritten: 0,
}, {
name: "html",
in: testRuleTextHTML,
wantErrMsg: rulelist.ErrHTML.Error(),
wantTitle: "",
wantRulesNum: 0,
wantWritten: 0,
}, {
name: "comments",
in: "# Comment 1\n" +
"! Comment 2\n",
wantErrMsg: "",
wantTitle: "",
wantRulesNum: 0,
wantWritten: 0,
}, {}, {
name: "rule",
in: testRuleTextBlocked,
wantDst: testRuleTextBlocked,
wantErrMsg: "",
wantRulesNum: 1,
wantTitle: "",
wantWritten: len(testRuleTextBlocked),
}, {
name: "html_in_rule",
in: testRuleTextBlocked + testRuleTextHTML,
wantDst: testRuleTextBlocked + testRuleTextHTML,
wantErrMsg: "",
wantTitle: "",
wantRulesNum: 2,
wantWritten: len(testRuleTextBlocked) + len(testRuleTextHTML),
}, {
name: "title",
in: "! Title: Test Title \n" +
"! Title: Bad, Ignored Title\n" +
testRuleTextBlocked,
wantDst: testRuleTextBlocked,
wantErrMsg: "",
wantTitle: "Test Title",
wantRulesNum: 1,
wantWritten: len(testRuleTextBlocked),
}, {
name: "bad_char",
in: "! Title: Test Title \n" +
testRuleTextBlocked +
">>>\x7F<<<",
wantDst: testRuleTextBlocked,
wantErrMsg: "line at index 2: " +
"character at index 3: " +
"non-printable character",
wantTitle: "Test Title",
wantRulesNum: 1,
wantWritten: len(testRuleTextBlocked),
}, {
name: "too_long",
in: strings.Repeat("a", rulelist.MaxRuleLen+1),
wantDst: "",
wantErrMsg: "scanning filter contents: " + bufio.ErrTooLong.Error(),
wantTitle: "",
wantRulesNum: 0,
wantWritten: 0,
}, {
name: "bad_tab_and_comment",
in: testRuleTextBadTab,
wantDst: testRuleTextBadTab,
wantErrMsg: "",
wantTitle: "",
wantRulesNum: 1,
wantWritten: len(testRuleTextBadTab),
}, {
name: "etc_hosts_tab_and_comment",
in: testRuleTextEtcHostsTab,
wantDst: testRuleTextEtcHostsTab,
wantErrMsg: "",
wantTitle: "",
wantRulesNum: 1,
wantWritten: len(testRuleTextEtcHostsTab),
}}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dst := &bytes.Buffer{}
buf := make([]byte, rulelist.MaxRuleLen)
p := rulelist.NewParser()
r, err := p.Parse(dst, strings.NewReader(tc.in), buf)
require.NotNil(t, r)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.wantDst, dst.String())
assert.Equal(t, tc.wantTitle, r.Title)
assert.Equal(t, tc.wantRulesNum, r.RulesCount)
assert.Equal(t, tc.wantWritten, r.BytesWritten)
if tc.wantWritten > 0 {
assert.NotZero(t, r.Checksum)
}
})
}
}
func TestParser_Parse_writeError(t *testing.T) {
t.Parallel()
dst := &aghtest.Writer{
OnWrite: func(b []byte) (n int, err error) {
return 1, errors.Error("test error")
},
}
buf := make([]byte, rulelist.MaxRuleLen)
p := rulelist.NewParser()
r, err := p.Parse(dst, strings.NewReader(testRuleTextBlocked), buf)
require.NotNil(t, r)
testutil.AssertErrorMsg(t, "writing rule line: test error", err)
assert.Equal(t, 1, r.BytesWritten)
}
func TestParser_Parse_checksums(t *testing.T) {
t.Parallel()
const (
withoutComments = testRuleTextBlocked
withComments = "! Some comment.\n" +
" " + testRuleTextBlocked +
"# Another comment.\n"
)
buf := make([]byte, rulelist.MaxRuleLen)
p := rulelist.NewParser()
r, err := p.Parse(&bytes.Buffer{}, strings.NewReader(withoutComments), buf)
require.NotNil(t, r)
require.NoError(t, err)
gotWithoutComments := r.Checksum
p = rulelist.NewParser()
r, err = p.Parse(&bytes.Buffer{}, strings.NewReader(withComments), buf)
require.NotNil(t, r)
require.NoError(t, err)
gotWithComments := r.Checksum
assert.Equal(t, gotWithoutComments, gotWithComments)
}
var (
resSink *rulelist.ParseResult
errSink error
)
func BenchmarkParser_Parse(b *testing.B) {
dst := &bytes.Buffer{}
src := strings.NewReader(strings.Repeat(testRuleTextBlocked, 1000))
buf := make([]byte, rulelist.MaxRuleLen)
p := rulelist.NewParser()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
resSink, errSink = p.Parse(dst, src, buf)
dst.Reset()
}
require.NoError(b, errSink)
require.NotNil(b, resSink)
}
func FuzzParser_Parse(f *testing.F) {
const n = 64
testCases := []string{
"",
"# Comment",
"! Comment",
"! Title ",
"! Title XXX",
testRuleTextEtcHostsTab,
testRuleTextHTML,
testRuleTextBlocked,
testRuleTextBadTab,
"1.2.3.4",
"1.2.3.4 etc-hosts.example",
">>>\x00<<<",
">>>\x7F<<<",
strings.Repeat("a", n+1),
}
for _, tc := range testCases {
f.Add(tc)
}
buf := make([]byte, n)
f.Fuzz(func(t *testing.T, input string) {
require.Eventually(t, func() (ok bool) {
dst := &bytes.Buffer{}
src := strings.NewReader(input)
p := rulelist.NewParser()
r, _ := p.Parse(dst, src, buf)
require.NotNil(t, r)
return true
}, testTimeout, testTimeout/100)
})
}

View File

@@ -0,0 +1,11 @@
// Package rulelist contains the implementation of the standard rule-list
// filter that wraps an urlfilter filtering-engine.
//
// TODO(a.garipov): Expand.
package rulelist
// MaxRuleLen is the maximum length of a line with a filtering rule, in bytes.
//
// TODO(a.garipov): Consider changing this to a rune length, like AdGuardDNS
// does.
const MaxRuleLen = 1024

View File

@@ -0,0 +1,14 @@
package rulelist_test
import "time"
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
// Common texts for tests.
const (
testRuleTextHTML = "<!DOCTYPE html>\n"
testRuleTextBlocked = "||blocked.example^\n"
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
testRuleTextEtcHostsTab = "0.0.0.0 tab..example^\t# A comment.\n"
)

View File

@@ -27,6 +27,25 @@ var blockedServices = []blockedService{{
"||9cache.com^",
"||9gag.com^",
},
}, {
ID: "activision_blizzard",
Name: "Activision Blizzard",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-237 0 1572 1572\"><path d=\"m549.1.2 548.4 1571.4H798l-74.2-200H374.5l-74.3 200H.7zM626 1085.1l-83-274.3-82.9 274.3z\"/></svg>"),
Rules: []string{
"||activision.com^",
"||activisionblizzard.com^",
"||demonware.net^",
},
}, {
ID: "aliexpress",
Name: "AliExpress",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M9 4C6.25 4 4 6.25 4 9v32c0 2.75 2.25 5 5 5h32c2.75 0 5-2.25 5-5V9c0-2.75-2.25-5-5-5H9zm0 2h32c1.668 0 3 1.332 3 3v3.38A3.973 3.973 0 0 0 41 11H9a3.973 3.973 0 0 0-3 1.38V9c0-1.668 1.332-3 3-3zm6 11a1 1 0 0 1 1 1c0 4.962 4.037 9 9 9s9-4.038 9-9a1 1 0 1 1 2 0c0 6.065-4.935 11-11 11s-11-4.935-11-11a1 1 0 0 1 1-1z\"/></svg>"),
Rules: []string{
"||ae-rus.net^",
"||ae-rus.ru^",
"||aliexpress.com^",
"||aliexpress.ru^",
},
}, {
ID: "amazon",
Name: "Amazon",
@@ -293,6 +312,21 @@ var blockedServices = []blockedService{{
"||mincdn.com^",
"||yo9.com^",
},
}, {
ID: "blizzard_entertainment",
Name: "Blizzard Entertainment",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 -32 128 128\"><path fill-rule=\"evenodd\" d=\"M105 2h3v1h2l2 1 1 1h3l1 1h4l1 1 2 2v1l1 3v4l1 2v6l-1 2v2l-1 3v2l-1 2v14l-1 2v1l-1 3-1 1h-3l-1 1h-6a5 5 0 0 0 1-6l2-1h-1l-1-3v-3a350 350 0 0 1 0-8l-1-3v-1l-1-1V9h1V6l-1-1-4-3Zm9 13v10h1v25a8 8 0 0 0 2-4l1-1 1-3V30l1-1v-2l1-1v-5l-1-2-2-3-1-1h-3Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M101 24v1l2 1h1v2h1l1 2v5l1 2s0-1 0 0l1 7 1 2v7l-1 5h-2l-2-2-4-1 1-3 1-2a22 22 0 0 0-1-10l-1-4h-1l1-4-1-1-2-3v2l-1 1v3l1 6v4l1 1-1 3v4l1 1-1 2v4l-1-1a13 13 0 0 0-4-5l-2-2 2-5V27l-1-1v-4l-1-1v-5h-1a33 33 0 0 1 0-4l4-4h-2l-4-4h-1V3h10l2 1 2 1h1c2 0 2 1 3 2l2 3 1 3v1l-1 2v1a11 11 0 0 1-1 4l-4 3ZM96 9v13l1 1a3 3 0 0 0 1-1c1 0 2-1 2-3v-1l1-1v-3l-2-3-2-2h-1ZM26 3l1 1h1l2 3v5l1 1v2l-1 1v9l1 1 1 1-1 7v9l-1 1 1 1-1 1v8h3l1-1h7v-1h16v6l1 2h-6l-1-1h-2l-1-1H31a4 4 0 0 0-3-1l-1 1h-1l-1 1h-5l1-1a10 10 0 0 0 3-2v-9l1-1-1-1V35l1-1V21l-1-1v-4l1-1v-3l1-2-1-3h-1l-2-2-1-1 1-1h4Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M84 60v-3l-1-2v-4l-3-2v-1l1-2a11 11 0 0 0 2-6l-1-1-3-2h-2v3l1 1h1l-1 2h-4l-2 1-2 1-1-2v-1l1-1 1-1 1-2v-5l1-1v-6l1-1v-3l1-1v-3l1-2 1-1-1-1 1-1 1-3 1-1V7l1-1c1-1 0-4 2-3l1 3 1 1 1 2v1l1 5 1 3v2l1 1v2l1 1v8l1 3v9l-1 1-2 5v3l-1 2v4l-1 1h-1Zm-4-36-1 1v2l-1 2v4l4 1h2v-7l-1-3-2-1-1-1v2Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M77 4v1l-2 3v2l-1 2v1l-1 1-1 4v7h-1v2a5 5 0 0 1-1 2v2l-2 2v7l-1 2v2l-2 4v3-1h3v-1l3-1 1-1 3-2h3l1 1-1 1a3 3 0 0 0 0 1l1 1v5l-1 1h-7v-1h-2l-2 1h-4l-2 1-1-2v-2l1-1v-1l1-1-1-1 1-1v-2l1-2-1-2v-8l1-2 2-5-1-1 1-2v-1l1-1v-4l1-1 2-4v-2l1-1h-3V8h-1l-1 1-2 3-1 4h-1l-1-1v-2l1-1V4h16ZM32 4h9l1 2-3 2 1 2-1 1v13l1 2-1 2v6l-1 1v2l1 1v5l-1 1 1 2 1 1 2 1v2h-7l-2 1-1-1 3-2v-8a4 4 0 0 1 0-2l1-1v-3l-1-14v-2l1-1h-1V7l-2-1h-1l-1-1 1-1Zm12 0h14v15c-2 1-2 4-3 6v2c-1 0-3 1-2 4h-1l-2 3-1 2v3l-1 2-2 5h2l1-1h2l1-1c1-1 1-3 3-3l1-2 2-2h1l1 3h-1v1l-1 1v7h-8l-1 1-2-1h-3l-1-1 1-1v-3l-1-2 1-1-1-1 1-3v-2l1-2 1-3a7 7 0 0 1 2-4l1-4 2-2 2-3v-3h1l2-3V8l-3-1h-2l-1 1a3 3 0 0 0-2 3l-1 1v4l-1 1-1 1v-1l-1-1V4ZM17 22l1 1h1v3s0-1 0 0l2 1v5l1 2-1 8v3a6 6 0 0 1 0 2l-1 2-1 2-1 3-3 2-2 2-3 1-1-1-1 1H1l-1-1 2-1 1-4V26l1-1-1-3V11l1-1-1-1H2V8L1 7 0 6V5l1-1h15l1 1c2 0 3 1 3 2l1 3v6l-4 6Zm-6-11v9h1l1-2 2-1v-6h-1l-1-1h-2v1Zm0 19-1 1 1 2-1 3v9a2 2 0 0 0 0 1v6l-1 1 3-1 1-2h1v-4l1-4v-5l-1-1 1-3-1-1v-2s0 1 0 0v-1l-1-1a20 20 0 0 1-2-2v4Z\" clip-rule=\"evenodd\"/></svg>"),
Rules: []string{
"||battle.net^",
"||battlenet.com.cn^",
"||blizzard.cn^",
"||blizzardgames.cn^",
"||blz-contentstack.com^",
"||blzstatic.cn^",
"||bnet.163.com^",
"||bnet.cn^",
"||lizzard.com^",
},
}, {
ID: "cloudflare",
Name: "CloudFlare",
@@ -329,6 +363,14 @@ var blockedServices = []blockedService{{
"||warp.plus^",
"||workers.dev^",
},
}, {
ID: "clubhouse",
Name: "Clubhouse",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M29.8 4a1 1 0 0 0-.92.7 1 1 0 0 0 .36 1.1 31.2 31.2 0 0 1 6 6.02 1 1 0 1 0 1.6-1.2 33.2 33.2 0 0 0-6.4-6.4A1 1 0 0 0 29.8 4Zm-7.16 1.06c-.46 0-.87.3-.99.74a1 1 0 0 0 .5 1.15 31.13 31.13 0 0 1 11.13 10.6 1 1 0 1 0 1.7-1.07A33.12 33.12 0 0 0 23.11 5.2a.96.96 0 0 0-.48-.14ZM14.5 7.01a3.42 3.42 0 0 0-3.27 2.28l-.26-.27A3.49 3.49 0 0 0 8.5 8.01c-.9 0-1.8.34-2.48 1.01a3.51 3.51 0 0 0-.57 4.17c-.52.15-1.01.42-1.43.84a3.52 3.52 0 0 0 0 4.94l.27.27c-.46.16-.9.41-1.27.79a3.52 3.52 0 0 0 0 4.94l.88.88 16.47 16.47a9.01 9.01 0 0 0 12.72 0l4.23-4.22a9.94 9.94 0 0 0 2.3-3.59l2.63-7.08a8.03 8.03 0 0 1 1.84-2.87l1.74-1.73 1-1a4.02 4.02 0 0 0 0-5.66 4.02 4.02 0 0 0-5.66 0l-1 1-.7.71-4.2 4.2a2.98 2.98 0 0 1-4.24 0L17.9 8.96l-.94-.94a3.49 3.49 0 0 0-2.47-1.01Zm0 1.98c.38 0 .76.15 1.06.45l.94.94 13.1 13.1a5.02 5.02 0 0 0 7.08 0l4.2-4.18.7-.71 1-1c.8-.8 2.05-.8 2.83 0 .8.79.8 2.04 0 2.83l-2.73 2.73a10.03 10.03 0 0 0-2.3 3.58l-2.63 7.08a8.02 8.02 0 0 1-1.84 2.87l-4.23 4.23a6.99 6.99 0 0 1-9.9 0L4.44 23.56a1.5 1.5 0 0 1 0-2.12c.59-.59 1.45-.55 2.08.08l.1.09 8.2 8.37a1 1 0 0 0 .97.29 1 1 0 0 0 .46-1.68l-9.52-9.73-.01-.01-1.28-1.29a1.5 1.5 0 0 1 0-2.12c.6-.6 1.47-.58 2.08.03l9.18 9.17a1 1 0 0 0 1.69-.43 1 1 0 0 0-.28-.98L9 14.13l-.06-.07-1.5-1.5c-.6-.6-.6-1.53 0-2.12a1.5 1.5 0 0 1 2.12 0L20.8 21.67a1 1 0 0 0 1.68-.44 1 1 0 0 0-.27-.97l-8.7-8.7-.06-.06a1.4 1.4 0 0 1-.01-2.06c.3-.3.68-.45 1.06-.45ZM4.23 32a1 1 0 0 0-.82 1.51c3 5.18 7.36 9.46 12.59 12.37a1 1 0 0 0 1.51-.89 1 1 0 0 0-.54-.86A31.16 31.16 0 0 1 5.15 32.5a1.01 1.01 0 0 0-.92-.51Z\"/></svg>"),
Rules: []string{
"||clubhouse.com^",
"||clubhouseapi.com^",
},
}, {
ID: "crunchyroll",
Name: "Crunchyroll",
@@ -736,6 +778,18 @@ var blockedServices = []blockedService{{
"||xxbay.com^",
"||yibei.org^",
},
}, {
ID: "electronic_arts",
Name: "Electronic Arts",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 1000 1000\"><path d=\"M500 1000C224.3 1000 0 775.7 0 500S224.3 0 500 0s500 224.3 500 500-224.3 500-500 500zm84.63-693.4H302.05l-42.87 68.9h282.25zm57.75.66L469.63 582.33H278.02l44.2-68.96h114.85l43.87-68.93h-265.5l-43.86 68.93h62.9L147.2 651.05h364.2L645.9 438.9l49.05 74.46h-44.23l-41.88 68.96H739.8l45.48 68.72h83.54z\"/></svg>"),
Rules: []string{
"||ea.com^",
"||eamobile.com^",
"||easports.com^",
"||nearpolar.com^",
"||swtor.com^",
"||tnt-ea.com^",
},
}, {
ID: "epic_games",
Name: "Epic Games",
@@ -1412,6 +1466,28 @@ var blockedServices = []blockedService{{
"||lineshoppingseller.com^",
"||linetv.tw^",
},
}, {
ID: "linkedin",
Name: "LinkedIn",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M41,4H9C6.24,4,4,6.24,4,9v32c0,2.76,2.24,5,5,5h32c2.76,0,5-2.24,5-5V9C46,6.24,43.76,4,41,4z M17,20v19h-6V20H17z M11,14.47c0-1.4,1.2-2.47,3-2.47s2.93,1.07,3,2.47c0,1.4-1.12,2.53-3,2.53C12.2,17,11,15.87,11,14.47z M39,39h-6c0,0,0-9.26,0-10 c0-2-1-4-3.5-4.04h-0.08C27,24.96,26,27.02,26,29c0,0.91,0,10,0,10h-6V20h6v2.56c0,0,1.93-2.56,5.81-2.56 c3.97,0,7.19,2.73,7.19,8.26V39z\"/></svg>"),
Rules: []string{
"||bizographics.com^",
"||cs1404.wpc.epsiloncdn.net^",
"||cs767.wpc.epsiloncdn.net^",
"||l-0005.dc-msedge.net^",
"||l-0005.l-dc-msedge.net^",
"||l-0005.l-msedge.net^",
"||l-0015.l-msedge.net^",
"||licdn.cn^",
"||licdn.com^",
"||linkedin.at^",
"||linkedin.be^",
"||linkedin.cn^",
"||linkedin.com^",
"||linkedin.nl^",
"||linkedin.qtlcdn.com^",
"||lnkd.in^",
},
}, {
ID: "mail_ru",
Name: "Mail.ru",
@@ -1429,6 +1505,7 @@ var blockedServices = []blockedService{{
"||aus.social^",
"||awscommunity.social^",
"||climatejustice.social^",
"||cupoftea.social^",
"||cyberplace.social^",
"||defcon.social^",
"||det.social^",
@@ -1470,12 +1547,12 @@ var blockedServices = []blockedService{{
"||mastodon.social^",
"||mastodon.uno^",
"||mastodon.world^",
"||mastodon.zaclys.com^",
"||mastodonapp.uk^",
"||mastodonners.nl^",
"||mastodont.cat^",
"||mastodontech.de^",
"||mastodontti.fi^",
"||mastouille.fr^",
"||mathstodon.xyz^",
"||metalhead.club^",
"||mindly.social^",
@@ -1485,13 +1562,13 @@ var blockedServices = []blockedService{{
"||mstdn.plus^",
"||mstdn.social^",
"||muenchen.social^",
"||muenster.im^",
"||nerdculture.de^",
"||noc.social^",
"||norden.social^",
"||nrw.social^",
"||o3o.ca^",
"||ohai.social^",
"||pewtix.com^",
"||piaille.fr^",
"||pol.social^",
"||ravenation.club^",
@@ -1506,7 +1583,6 @@ var blockedServices = []blockedService{{
"||social.linux.pizza^",
"||social.politicaconciencia.org^",
"||social.vivaldi.net^",
"||sself.co^",
"||stranger.social^",
"||sueden.social^",
"||tech.lgbt^",
@@ -1566,6 +1642,44 @@ var blockedServices = []blockedService{{
"||nflxso.net^",
"||nflxvideo.net^",
},
}, {
ID: "nintendo",
Name: "Nintendo",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M6 7v36h12.6V21.75l13 20.78.27.47H44V7H31.4v1l.04 20.22L18.5 7.47 18.22 7Zm2 2h9.1l14.5 23.22 1.84 3v-3.5L33.4 9H42v32h-9L18.44 17.75l-1.85-2.94V41H8Z\"/></svg>"),
Rules: []string{
"||nintendo-europe.com^",
"||nintendo.be^",
"||nintendo.co.jp^",
"||nintendo.co.uk^",
"||nintendo.com.au^",
"||nintendo.com^",
"||nintendo.de^",
"||nintendo.es^",
"||nintendo.eu^",
"||nintendo.fr^",
"||nintendo.it^",
"||nintendo.jp^",
"||nintendo.net^",
"||nintendo.nl^",
"||nintendoswitch.cn^",
"||nintendowifi.net^",
},
}, {
ID: "nvidia",
Name: "Nvidia",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 48 48\"><path d=\"M20 8a2 2 0 0 0-2 2v2.55l.84-.05c10.76-.37 17.78 8.82 17.78 8.82s-8.05 9.8-16.44 9.8c-.73 0-1.47-.07-2.18-.19v-2.2c.73.23 1.52.35 2.3.35 5.88 0 11.35-7.6 11.35-7.6s-5.07-6.91-12.81-6.66l-.82.03v-2.3c-9.49.77-17.68 8.8-17.68 8.8S4.97 34.76 18 35.98v-2.44c.59.07 1.22.12 1.81.12 7.82 0 13.47-3.99 18.94-8.7.91.73 4.62 2.49 5.4 3.26-5.2 4.36-17.33 7.86-24.2 7.86-.66 0-1.32-.03-1.95-.1V38c0 1.1.9 2 2 2h25a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H20zm-2 6.86v2.82a11.8 11.8 0 0 1 1.57-.07c4.95 0 7.9 3.85 7.9 3.85l-4.03 3.39c-1.8-3.02-2.43-4.35-5.44-4.7v8.57c-4.06-1.38-5.4-6.14-5.4-6.14s2.37-2.83 5.38-2.46H18v-2.44a15.66 15.66 0 0 0-9.22 4.46s2 7.52 9.22 8.8v2.6c-9.56-1.17-12.82-11.7-12.82-11.7s4.27-6.3 12.82-6.97z\"/></svg>"),
Rules: []string{
"||geforce.com^",
"||geforcenow.com^",
"||nvidia.cn^",
"||nvidia.com.global.ogslb.com^",
"||nvidia.com^",
"||nvidia.eu^",
"||nvidia.partners^",
"||nvidiagrid.net^",
"||nvidianews.com^",
"||tegrazone.com^",
},
}, {
ID: "ok",
Name: "OK.ru",
@@ -1964,6 +2078,16 @@ var blockedServices = []blockedService{{
"||twvid.com^",
"||vine.co^",
},
}, {
ID: "ubisoft",
Name: "Ubisoft",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 32 32\"><path d=\"M15.22 3C7.14 3 3.66 10.18 3.66 10.18l1.03.74s-1.3 2.45-1.26 5.6A12.5 12.5 0 0 0 16.08 29a12.5 12.5 0 0 0 12.49-12.46c0-9-6.98-13.54-13.35-13.54zm.07 2.2c6.3 0 11.2 5.07 11.2 10.98 0 6.27-4.71 10.62-10.2 10.62-4.04 0-7.69-3.08-7.69-7.3a5.8 5.8 0 0 1 2.75-5.03l.21.23a6.37 6.37 0 0 0-1.53 3.91c0 3.32 2.6 5.62 5.88 5.62 4.18 0 6.97-3.56 6.97-7.7 0-4.81-4.25-8.9-9.36-8.9a11.1 11.1 0 0 0-6.61 2.3l-.21-.2a10.07 10.07 0 0 1 8.59-4.54zM13.4 9.8c3.26 0 6.44 2.15 7.24 5.22l-.3.1a8.35 8.35 0 0 0-6.52-3.44c-5.08 0-7.75 4.62-7.36 8.47l-.3.12s-.56-1.24-.56-2.71a7.8 7.8 0 0 1 7.8-7.76zm2.15 5.33a2.77 2.77 0 0 1 2.78 2.74c0 1.23-.79 1.96-.79 1.96l.94.65s-.93 1.46-2.82 1.46a3.4 3.4 0 0 1-.1-6.8z\"/></svg>"),
Rules: []string{
"||ubi.com^",
"||ubisoft.com^",
"||ubisoft.org^",
"||ubisoftconnect.com^",
},
}, {
ID: "valorant",
Name: "Valorant",

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/schedule"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
)
@@ -118,13 +119,18 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
}
}
weekly := schedule.EmptyWeekly()
if prev != nil {
weekly = prev.BlockedServices.Schedule.Clone()
}
c = &Client{
safeSearchConf: safeSearchConf,
Name: cj.Name,
BlockedServices: &filtering.BlockedServices{
Schedule: prev.BlockedServices.Schedule.Clone(),
Schedule: weekly,
IDs: cj.BlockedServices,
},

View File

@@ -30,32 +30,30 @@ import (
const dataDir = "data"
// logSettings are the logging settings part of the configuration file.
//
// TODO(a.garipov): Put them into a separate object.
type logSettings struct {
// File is the path to the log file. If empty, logs are written to stdout.
// If "syslog", logs are written to syslog.
File string `yaml:"log_file"`
File string `yaml:"file"`
// MaxBackups is the maximum number of old log files to retain.
//
// NOTE: MaxAge may still cause them to get deleted.
MaxBackups int `yaml:"log_max_backups"`
MaxBackups int `yaml:"max_backups"`
// MaxSize is the maximum size of the log file before it gets rotated, in
// megabytes. The default value is 100 MB.
MaxSize int `yaml:"log_max_size"`
MaxSize int `yaml:"max_size"`
// MaxAge is the maximum duration for retaining old log files, in days.
MaxAge int `yaml:"log_max_age"`
MaxAge int `yaml:"max_age"`
// Compress determines, if the rotated log files should be compressed using
// gzip.
Compress bool `yaml:"log_compress"`
Compress bool `yaml:"compress"`
// LocalTime determines, if the time used for formatting the timestamps in
// is the computer's local time.
LocalTime bool `yaml:"log_localtime"`
LocalTime bool `yaml:"local_time"`
// Verbose determines, if verbose (aka debug) logging is enabled.
Verbose bool `yaml:"verbose"`
@@ -91,18 +89,17 @@ type clientSourcesConfig struct {
HostsFile bool `yaml:"hosts"`
}
// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
// configuration is loaded from YAML.
//
// Field ordering is important, YAML fields better not to be reordered, if it's
// not absolutely necessary.
type configuration struct {
// Raw file data to avoid re-reading of configuration file
// It's reset after config is parsed
fileData []byte
// BindHost is the address for the web interface server to listen on.
BindHost netip.Addr `yaml:"bind_host"`
// BindPort is the port for the web interface server to listen on.
BindPort int `yaml:"bind_port"`
// HTTPConfig is the block with http conf.
HTTPConfig httpConfig `yaml:"http"`
// Users are the clients capable for accessing the web interface.
Users []webUser `yaml:"users"`
// AuthAttempts is the maximum number of failed login attempts a user
@@ -120,10 +117,6 @@ type configuration struct {
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
DebugPProf bool `yaml:"debug_pprof"`
// TTL for a web session (in hours)
// An active session is automatically refreshed once a day.
WebSessionTTLHours uint32 `yaml:"web_session_ttl"`
DNS dnsConfig `yaml:"dns"`
TLS tlsConfigSettings `yaml:"tls"`
QueryLog queryLogConfig `yaml:"querylog"`
@@ -147,7 +140,8 @@ type configuration struct {
// Keep this field sorted to ensure consistent ordering.
Clients *clientsConfig `yaml:"clients"`
logSettings `yaml:",inline"`
// Log is a block with log configuration settings.
Log logSettings `yaml:"log"`
OSConfig *osConfig `yaml:"os"`
@@ -156,7 +150,23 @@ type configuration struct {
SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
}
// field ordering is important -- yaml fields will mirror ordering from here
// httpConfig is a block with HTTP configuration params.
//
// Field ordering is important, YAML fields better not to be reordered, if it's
// not absolutely necessary.
type httpConfig struct {
// Address is the address to serve the web UI on.
Address netip.AddrPort
// SessionTTL for a web session.
// An active session is automatically refreshed once a day.
SessionTTL timeutil.Duration `yaml:"session_ttl"`
}
// dnsConfig is a block with DNS configuration params.
//
// Field ordering is important, YAML fields better not to be reordered, if it's
// not absolutely necessary.
type dnsConfig struct {
BindHosts []netip.Addr `yaml:"bind_hosts"`
Port int `yaml:"port"`
@@ -261,11 +271,12 @@ type statsConfig struct {
//
// TODO(a.garipov, e.burkov): This global is awful and must be removed.
var config = &configuration{
BindPort: 3000,
BindHost: netip.IPv4Unspecified(),
AuthAttempts: 5,
AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24,
AuthAttempts: 5,
AuthBlockMin: 15,
HTTPConfig: httpConfig{
Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),
SessionTTL: timeutil.Duration{Duration: 30 * timeutil.Day},
},
DNS: dnsConfig{
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS,
@@ -378,7 +389,7 @@ var config = &configuration{
HostsFile: true,
},
},
logSettings: logSettings{
Log: logSettings{
Compress: false,
LocalTime: false,
MaxBackups: 0,
@@ -409,26 +420,26 @@ func (c *configuration) getConfigFilename() string {
// separate method in order to configure logger before the actual configuration
// is parsed and applied.
func readLogSettings() (ls *logSettings) {
ls = &logSettings{}
conf := &configuration{}
yamlFile, err := readConfigFile()
if err != nil {
return ls
return &logSettings{}
}
err = yaml.Unmarshal(yamlFile, ls)
err = yaml.Unmarshal(yamlFile, conf)
if err != nil {
log.Error("Couldn't get logging settings from the configuration: %s", err)
}
return ls
return &conf.Log
}
// validateBindHosts returns error if any of binding hosts from configuration is
// not a valid IP address.
func validateBindHosts(conf *configuration) (err error) {
if !conf.BindHost.IsValid() {
return errors.Error("bind_host is not a valid ip address")
if !conf.HTTPConfig.Address.IsValid() {
return errors.Error("http.address is not a valid ip address")
}
for i, addr := range conf.DNS.BindHosts {
@@ -462,7 +473,7 @@ func parseConfig() (err error) {
}
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(config.BindPort))
addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))

View File

@@ -103,7 +103,7 @@ type statusResponse struct {
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
HTTPPort uint16 `json:"http_port"`
// ProtectionDisabledDuration is the duration of the protection pause in
// milliseconds.
@@ -158,7 +158,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
Language: config.Language,
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
HTTPPort: config.HTTPConfig.Address.Port(),
ProtectionDisabledDuration: protectionDisabledDuration,
ProtectionEnabled: protectionEnabled,
IsRunning: isRunning(),

View File

@@ -96,8 +96,9 @@ type checkConfResp struct {
func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err error) {
defer func() { err = errors.Annotate(err, "validating ports: %w") }()
portInt := req.Web.Port
port := tcpPort(portInt)
// TODO(a.garipov): Declare all port variables anywhere as uint16.
reqPort := uint16(req.Web.Port)
port := tcpPort(reqPort)
addPorts(tcpPorts, port)
if err = tcpPorts.Validate(); err != nil {
// Reset the value for the port to 1 to make sure that validateDNS
@@ -108,15 +109,15 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
return err
}
switch portInt {
case 0, config.BindPort:
switch reqPort {
case 0, config.HTTPConfig.Address.Port():
return nil
default:
// Go on and check the port binding only if it's not zero or won't be
// unbound after install.
}
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, reqPort))
}
// validateDNS returns error if the DNS part of the initial configuration can't
@@ -127,11 +128,11 @@ func (req *checkConfReq) validateDNS(
) (canAutofix bool, err error) {
defer func() { err = errors.Annotate(err, "validating ports: %w") }()
port := req.DNS.Port
port := uint16(req.DNS.Port)
switch port {
case 0:
return false, nil
case config.BindPort:
case config.HTTPConfig.Address.Port():
// Go on and only check the UDP port since the TCP one is already bound
// by AdGuard Home for web interface.
default:
@@ -318,8 +319,7 @@ type applyConfigReq struct {
// copyInstallSettings copies the installation parameters between two
// configuration structures.
func copyInstallSettings(dst, src *configuration) {
dst.BindHost = src.BindHost
dst.BindPort = src.BindPort
dst.HTTPConfig = src.HTTPConfig
dst.DNS.BindHosts = src.DNS.BindHosts
dst.DNS.Port = src.DNS.Port
}
@@ -413,8 +413,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
copyInstallSettings(curConfig, config)
Context.firstRun = false
config.BindHost = req.Web.IP
config.BindPort = req.Web.Port
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port))
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
config.DNS.Port = req.DNS.Port
@@ -487,7 +486,8 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
return nil, false, errors.Error("ports cannot be 0")
}
restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
addrPort := config.HTTPConfig.Address
restartHTTP = addrPort.Addr() != req.Web.IP || int(addrPort.Port()) != req.Web.Port
if restartHTTP {
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
if err != nil {

View File

@@ -157,7 +157,9 @@ func (vr *versionResponse) setAllowedToAutoUpdate() (err error) {
Context.tls.WriteDiskConfig(tlsConf)
canUpdate := true
if tlsConfUsesPrivilegedPorts(tlsConf) || config.BindPort < 1024 || config.DNS.Port < 1024 {
if tlsConfUsesPrivilegedPorts(tlsConf) ||
config.HTTPConfig.Address.Port() < 1024 ||
config.DNS.Port < 1024 {
canUpdate, err = aghnet.CanBindPrivilegedPorts()
if err != nil {
return fmt.Errorf("checking ability to bind privileged ports: %w", err)

View File

@@ -17,6 +17,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/rdns"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
@@ -167,30 +168,77 @@ func initDNSServer(
return fmt.Errorf("dnsServer.Prepare: %w", err)
}
if config.Clients.Sources.RDNS {
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
}
initRDNS()
initWHOIS()
return nil
}
const (
// defaultQueueSize is the size of queue of IPs for rDNS and WHOIS
// processing.
defaultQueueSize = 255
// defaultCacheSize is the maximum size of the cache for rDNS and WHOIS
// processing. It must be greater than zero.
defaultCacheSize = 10_000
// defaultIPTTL is the Time to Live duration for IP addresses cached by
// rDNS and WHOIS.
defaultIPTTL = 1 * time.Hour
)
// initRDNS initializes the rDNS.
func initRDNS() {
Context.rdnsCh = make(chan netip.Addr, defaultQueueSize)
// TODO(s.chzhen): Add ability to disable it on dns server configuration
// update in [dnsforward] package.
r := rdns.New(&rdns.Config{
Exchanger: Context.dnsServer,
CacheSize: defaultCacheSize,
CacheTTL: defaultIPTTL,
})
go processRDNS(r)
}
// processRDNS processes reverse DNS lookup queries. It is intended to be used
// as a goroutine.
func processRDNS(r rdns.Interface) {
defer log.OnPanic("rdns")
for ip := range Context.rdnsCh {
ok := Context.dnsServer.ShouldResolveClient(ip)
if !ok {
continue
}
host, changed := r.Process(ip)
if host == "" || !changed {
continue
}
ok = Context.clients.AddHost(ip, host, ClientSourceRDNS)
if ok {
continue
}
log.Debug(
"dns: can't set rdns info for client %q: already set with higher priority source",
ip,
)
}
}
// 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
@@ -200,9 +248,6 @@ func initWHOIS() {
// 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)
@@ -274,11 +319,7 @@ func onDNSRequest(pctx *proxy.DNSContext) {
return
}
srcs := config.Clients.Sources
if srcs.RDNS && !ip.IsLoopback() {
Context.rdns.Begin(ip)
}
Context.rdnsCh <- ip
Context.whoisCh <- ip
}
@@ -517,11 +558,7 @@ func startDNSServer() error {
const topClientsNumber = 100 // the number of clients to get
for _, ip := range Context.stats.TopClientsIP(topClientsNumber) {
srcs := config.Clients.Sources
if srcs.RDNS && !ip.IsLoopback() {
Context.rdns.Begin(ip)
}
Context.rdnsCh <- ip
Context.whoisCh <- ip
}

View File

@@ -56,7 +56,6 @@ type homeContext struct {
stats stats.Interface // statistics module
queryLog querylog.QueryLog // query log module
dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module
dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering module
@@ -83,6 +82,9 @@ type homeContext struct {
client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
// rdnsCh is the channel for receiving IPs for rDNS processing.
rdnsCh chan netip.Addr
// whoisCh is the channel for receiving IPs for WHOIS processing.
whoisCh chan netip.Addr
@@ -372,8 +374,26 @@ func initContextClients() (err error) {
// setupBindOpts overrides bind host/port from the opts.
func setupBindOpts(opts options) (err error) {
bindAddr := opts.bindAddr
if bindAddr != (netip.AddrPort{}) {
config.HTTPConfig.Address = bindAddr
if config.HTTPConfig.Address.Port() != 0 {
err = checkPorts()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
}
return nil
}
if opts.bindPort != 0 {
config.BindPort = opts.bindPort
config.HTTPConfig.Address = netip.AddrPortFrom(
config.HTTPConfig.Address.Addr(),
uint16(opts.bindPort),
)
err = checkPorts()
if err != nil {
@@ -383,20 +403,10 @@ func setupBindOpts(opts options) (err error) {
}
if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost
}
// Rewrite deprecated options.
bindAddr := opts.bindAddr
if bindAddr.IsValid() {
config.BindHost = bindAddr.Addr()
config.BindPort = int(bindAddr.Port())
err = checkPorts()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
config.HTTPConfig.Address = netip.AddrPortFrom(
opts.bindHost,
config.HTTPConfig.Address.Port(),
)
}
return nil
@@ -460,7 +470,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
ServiceName: pcService,
TXTSuffix: pcTXTSuffix,
CacheTime: cacheTime,
CacheSize: conf.SafeBrowsingCacheSize,
CacheSize: conf.ParentalCacheSize,
})
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
@@ -480,7 +490,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
// checkPorts is a helper for ports validation in config.
func checkPorts() (err error) {
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(config.BindPort))
addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))
@@ -520,8 +530,8 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) {
webConf := webConfig{
firstRun: Context.firstRun,
BindHost: config.BindHost,
BindPort: config.BindPort,
BindHost: config.HTTPConfig.Address.Addr(),
BindPort: int(config.HTTPConfig.Address.Port()),
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHdrTimeout,
@@ -657,8 +667,8 @@ func initUsers() (auth *Auth, err error) {
log.Info("authratelimiter is disabled")
}
sessionTTL := config.WebSessionTTLHours * 60 * 60
auth = InitAuth(sessFilename, config.Users, sessionTTL, rateLimiter)
sessionTTL := config.HTTPConfig.SessionTTL.Seconds()
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter)
if auth == nil {
return nil, errors.Error("initializing auth module failed")
}
@@ -821,20 +831,21 @@ func configureLogger(opts options) (err error) {
// getLogSettings returns a log settings object properly initialized from opts.
func getLogSettings(opts options) (ls *logSettings) {
ls = readLogSettings()
configLogSettings := config.Log
// Command-line arguments can override config settings.
if opts.verbose || config.Verbose {
if opts.verbose || configLogSettings.Verbose {
ls.Verbose = true
}
ls.File = stringutil.Coalesce(opts.logFile, config.File, ls.File)
ls.File = stringutil.Coalesce(opts.logFile, configLogSettings.File, ls.File)
// Handle default log settings overrides.
ls.Compress = config.Compress
ls.LocalTime = config.LocalTime
ls.MaxBackups = config.MaxBackups
ls.MaxSize = config.MaxSize
ls.MaxAge = config.MaxAge
ls.Compress = configLogSettings.Compress
ls.LocalTime = configLogSettings.LocalTime
ls.MaxBackups = configLogSettings.MaxBackups
ls.MaxSize = configLogSettings.MaxSize
ls.MaxAge = configLogSettings.MaxAge
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if
@@ -936,7 +947,7 @@ func printHTTPAddresses(proto string) {
Context.tls.WriteDiskConfig(&tlsConf)
}
port := config.BindPort
port := int(config.HTTPConfig.Address.Port())
if proto == aghhttp.SchemeHTTPS {
port = tlsConf.PortHTTPS
}
@@ -948,9 +959,9 @@ func printHTTPAddresses(proto string) {
return
}
bindhost := config.BindHost
if !bindhost.IsUnspecified() {
printWebAddrs(proto, bindhost.String(), port)
bindHost := config.HTTPConfig.Address.Addr()
if !bindHost.IsUnspecified() {
printWebAddrs(proto, bindHost.String(), port)
return
}
@@ -961,14 +972,14 @@ func printHTTPAddresses(proto string) {
// That's weird, but we'll ignore it.
//
// TODO(e.burkov): Find out when it happens.
printWebAddrs(proto, bindhost.String(), port)
printWebAddrs(proto, bindHost.String(), port)
return
}
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
printWebAddrs(proto, addr.String(), config.BindPort)
printWebAddrs(proto, addr.String(), port)
}
}
}

View File

@@ -1,143 +0,0 @@
package home
import (
"encoding/binary"
"net/netip"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// RDNS resolves clients' addresses to enrich their metadata.
type RDNS struct {
exchanger dnsforward.RDNSExchanger
clients *clientsContainer
// ipCh used to pass client's IP to rDNS workerLoop.
ipCh chan netip.Addr
// ipCache caches the IP addresses to be resolved by rDNS. The resolved
// address stays here while it's inside clients. After leaving clients the
// address will be resolved once again. If the address couldn't be
// resolved, cache prevents further attempts to resolve it for some time.
ipCache cache.Cache
// usePrivate stores the state of current private reverse-DNS resolving
// settings.
usePrivate atomic.Bool
}
// Default AdGuard Home reverse DNS values.
const (
revDNSCacheSize = 10000
// TODO(e.burkov): Make these values configurable.
revDNSCacheTTL = 24 * 60 * 60
revDNSFailureCacheTTL = 1 * 60 * 60
revDNSQueueSize = 256
)
// NewRDNS creates and returns initialized RDNS.
func NewRDNS(
exchanger dnsforward.RDNSExchanger,
clients *clientsContainer,
usePrivate bool,
) (rDNS *RDNS) {
rDNS = &RDNS{
exchanger: exchanger,
clients: clients,
ipCache: cache.New(cache.Config{
EnableLRU: true,
MaxCount: revDNSCacheSize,
}),
ipCh: make(chan netip.Addr, revDNSQueueSize),
}
rDNS.usePrivate.Store(usePrivate)
go rDNS.workerLoop()
return rDNS
}
// ensurePrivateCache ensures that the state of the RDNS cache is consistent
// with the current private client RDNS resolving settings.
//
// TODO(e.burkov): Clearing cache each time this value changed is not a perfect
// approach since only unresolved locally-served addresses should be removed.
// Implement when improving the cache.
func (r *RDNS) ensurePrivateCache() {
usePrivate := r.exchanger.ResolvesPrivatePTR()
if r.usePrivate.CompareAndSwap(!usePrivate, usePrivate) {
r.ipCache.Clear()
}
}
// isCached returns true if ip is already cached and not expired yet. It also
// caches it otherwise.
func (r *RDNS) isCached(ip netip.Addr) (ok bool) {
ipBytes := ip.AsSlice()
now := uint64(time.Now().Unix())
if expire := r.ipCache.Get(ipBytes); len(expire) != 0 {
return binary.BigEndian.Uint64(expire) > now
}
return false
}
// cache caches the ip address for ttl seconds.
func (r *RDNS) cache(ip netip.Addr, ttl uint64) {
ipData := ip.AsSlice()
ttlData := [8]byte{}
binary.BigEndian.PutUint64(ttlData[:], uint64(time.Now().Unix())+ttl)
r.ipCache.Set(ipData, ttlData[:])
}
// Begin adds the ip to the resolving queue if it is not cached or already
// resolved.
func (r *RDNS) Begin(ip netip.Addr) {
r.ensurePrivateCache()
if r.isCached(ip) || r.clients.clientSource(ip) > ClientSourceRDNS {
return
}
select {
case r.ipCh <- ip:
log.Debug("rdns: %q added to queue", ip)
default:
log.Debug("rdns: queue is full")
}
}
// workerLoop handles incoming IP addresses from ipChan and adds it into
// clients.
func (r *RDNS) workerLoop() {
defer log.OnPanic("rdns")
for ip := range r.ipCh {
ttl := uint64(revDNSCacheTTL)
host, err := r.exchanger.Exchange(ip.AsSlice())
if err != nil {
log.Debug("rdns: resolving %q: %s", ip, err)
if errors.Is(err, dnsforward.ErrRDNSFailed) {
// Cache failure for a less time.
ttl = revDNSFailureCacheTTL
}
}
r.cache(ip, ttl)
if host != "" {
_ = r.clients.AddHost(ip, host, ClientSourceRDNS)
}
}
}

View File

@@ -1,264 +0,0 @@
package home
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"net/netip"
"sync"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRDNS_Begin(t *testing.T) {
aghtest.ReplaceLogLevel(t, log.DEBUG)
w := &bytes.Buffer{}
aghtest.ReplaceLogWriter(t, w)
ip1234, ip1235 := netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("1.2.3.5")
testCases := []struct {
cliIDIndex map[string]*Client
customChan chan netip.Addr
name string
wantLog string
ip netip.Addr
wantCacheHit int
wantCacheMiss int
}{{
cliIDIndex: map[string]*Client{},
customChan: nil,
name: "cached",
wantLog: "",
ip: ip1234,
wantCacheHit: 1,
wantCacheMiss: 0,
}, {
cliIDIndex: map[string]*Client{},
customChan: nil,
name: "not_cached",
wantLog: "rdns: queue is full",
ip: ip1235,
wantCacheHit: 0,
wantCacheMiss: 1,
}, {
cliIDIndex: map[string]*Client{"1.2.3.5": {}},
customChan: nil,
name: "already_in_clients",
wantLog: "",
ip: ip1235,
wantCacheHit: 0,
wantCacheMiss: 1,
}, {
cliIDIndex: map[string]*Client{},
customChan: make(chan netip.Addr, 1),
name: "add_to_queue",
wantLog: `rdns: "1.2.3.5" added to queue`,
ip: ip1235,
wantCacheHit: 0,
wantCacheMiss: 1,
}}
for _, tc := range testCases {
w.Reset()
ipCache := cache.New(cache.Config{
EnableLRU: true,
MaxCount: revDNSCacheSize,
})
ttl := make([]byte, binary.Size(uint64(0)))
binary.BigEndian.PutUint64(ttl, uint64(time.Now().Add(100*time.Hour).Unix()))
rdns := &RDNS{
ipCache: ipCache,
exchanger: &rDNSExchanger{
ex: aghtest.NewErrorUpstream(),
},
clients: &clientsContainer{
list: map[string]*Client{},
idIndex: tc.cliIDIndex,
ipToRC: map[netip.Addr]*RuntimeClient{},
allTags: stringutil.NewSet(),
},
}
ipCache.Clear()
ipCache.Set(net.IP{1, 2, 3, 4}, ttl)
if tc.customChan != nil {
rdns.ipCh = tc.customChan
defer close(tc.customChan)
}
t.Run(tc.name, func(t *testing.T) {
rdns.Begin(tc.ip)
assert.Equal(t, tc.wantCacheHit, ipCache.Stats().Hit)
assert.Equal(t, tc.wantCacheMiss, ipCache.Stats().Miss)
assert.Contains(t, w.String(), tc.wantLog)
})
}
}
// rDNSExchanger is a mock dnsforward.RDNSExchanger implementation for tests.
type rDNSExchanger struct {
ex upstream.Upstream
usePrivate bool
}
// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
func (e *rDNSExchanger) Exchange(ip net.IP) (host string, err error) {
rev, err := netutil.IPToReversedAddr(ip)
if err != nil {
return "", fmt.Errorf("reversing ip: %w", err)
}
req := &dns.Msg{
Question: []dns.Question{{
Name: dns.Fqdn(rev),
Qclass: dns.ClassINET,
Qtype: dns.TypePTR,
}},
}
resp, err := e.ex.Exchange(req)
if err != nil {
return "", err
}
if len(resp.Answer) == 0 {
return "", nil
}
return resp.Answer[0].Header().Name, nil
}
// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
func (e *rDNSExchanger) ResolvesPrivatePTR() (ok bool) {
return e.usePrivate
}
func TestRDNS_ensurePrivateCache(t *testing.T) {
data := []byte{1, 2, 3, 4}
ipCache := cache.New(cache.Config{
EnableLRU: true,
MaxCount: revDNSCacheSize,
})
ex := &rDNSExchanger{
ex: aghtest.NewErrorUpstream(),
}
rdns := &RDNS{
ipCache: ipCache,
exchanger: ex,
}
rdns.ipCache.Set(data, data)
require.NotZero(t, rdns.ipCache.Stats().Count)
ex.usePrivate = !ex.usePrivate
rdns.ensurePrivateCache()
require.Zero(t, rdns.ipCache.Stats().Count)
}
func TestRDNS_WorkerLoop(t *testing.T) {
aghtest.ReplaceLogLevel(t, log.DEBUG)
w := &bytes.Buffer{}
aghtest.ReplaceLogWriter(t, w)
localIP := netip.MustParseAddr("192.168.1.1")
revIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
require.NoError(t, err)
revIPv6, err := netutil.IPToReversedAddr(net.ParseIP("2a00:1450:400c:c06::93"))
require.NoError(t, err)
locUpstream := &aghtest.UpstreamMock{
OnAddress: func() (addr string) { return "local.upstream.example" },
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
return aghalg.Coalesce(
aghtest.MatchedResponse(req, dns.TypePTR, revIPv4, "local.domain"),
aghtest.MatchedResponse(req, dns.TypePTR, revIPv6, "ipv6.domain"),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
), nil
},
}
errUpstream := aghtest.NewErrorUpstream()
testCases := []struct {
ups upstream.Upstream
cliIP netip.Addr
wantLog string
name string
wantClientSource clientSource
}{{
ups: locUpstream,
cliIP: localIP,
wantLog: "",
name: "all_good",
wantClientSource: ClientSourceRDNS,
}, {
ups: errUpstream,
cliIP: netip.MustParseAddr("192.168.1.2"),
wantLog: `rdns: resolving "192.168.1.2": test upstream error`,
name: "resolve_error",
wantClientSource: ClientSourceNone,
}, {
ups: locUpstream,
cliIP: netip.MustParseAddr("2a00:1450:400c:c06::93"),
wantLog: "",
name: "ipv6_good",
wantClientSource: ClientSourceRDNS,
}}
for _, tc := range testCases {
w.Reset()
cc := newClientsContainer(t)
ch := make(chan netip.Addr)
rdns := &RDNS{
exchanger: &rDNSExchanger{
ex: tc.ups,
},
clients: cc,
ipCh: ch,
ipCache: cache.New(cache.Config{
EnableLRU: true,
MaxCount: revDNSCacheSize,
}),
}
t.Run(tc.name, func(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
rdns.workerLoop()
wg.Done()
}()
ch <- tc.cliIP
close(ch)
wg.Wait()
if tc.wantLog != "" {
assert.Contains(t, w.String(), tc.wantLog)
}
assert.Equal(t, tc.wantClientSource, cc.clientSource(tc.cliIP))
})
}
}

View File

@@ -320,7 +320,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
if setts.Enabled {
err = validatePorts(
tcpPort(config.BindPort),
tcpPort(config.HTTPConfig.Address.Port()),
tcpPort(setts.PortHTTPS),
tcpPort(setts.PortDNSOverTLS),
tcpPort(setts.PortDNSCrypt),
@@ -407,7 +407,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
if req.Enabled {
err = validatePorts(
tcpPort(config.BindPort),
tcpPort(config.HTTPConfig.Address.Port()),
tcpPort(req.PortHTTPS),
tcpPort(req.PortDNSOverTLS),
tcpPort(req.PortDNSCrypt),

View File

@@ -3,6 +3,7 @@ package home
import (
"bytes"
"fmt"
"net/netip"
"net/url"
"os"
"path"
@@ -22,7 +23,7 @@ import (
)
// currentSchemaVersion is the current schema version.
const currentSchemaVersion = 22
const currentSchemaVersion = 24
// These aliases are provided for convenience.
type (
@@ -96,6 +97,8 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema19to20,
upgradeSchema20to21,
upgradeSchema21to22,
upgradeSchema22to23,
upgradeSchema23to24,
}
n := 0
@@ -1256,6 +1259,177 @@ func upgradeSchema21to22(diskConf yobj) (err error) {
return nil
}
// upgradeSchema22to23 performs the following changes:
//
// # BEFORE:
// 'bind_host': '1.2.3.4'
// 'bind_port': 8080
// 'web_session_ttl': 720
//
// # AFTER:
// 'http':
// 'address': '1.2.3.4:8080'
// 'session_ttl': '720h'
func upgradeSchema22to23(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 22 to 23")
diskConf["schema_version"] = 23
bindHostVal, ok := diskConf["bind_host"]
if !ok {
return nil
}
bindHost, ok := bindHostVal.(string)
if !ok {
return fmt.Errorf("unexpected type of bind_host: %T", bindHostVal)
}
bindHostAddr, err := netip.ParseAddr(bindHost)
if err != nil {
return fmt.Errorf("invalid bind_host value: %s", bindHost)
}
bindPortVal, ok := diskConf["bind_port"]
if !ok {
return nil
}
bindPort, ok := bindPortVal.(int)
if !ok {
return fmt.Errorf("unexpected type of bind_port: %T", bindPortVal)
}
sessionTTLVal, ok := diskConf["web_session_ttl"]
if !ok {
return nil
}
sessionTTL, ok := sessionTTLVal.(int)
if !ok {
return fmt.Errorf("unexpected type of web_session_ttl: %T", sessionTTLVal)
}
addr := netip.AddrPortFrom(bindHostAddr, uint16(bindPort))
if !addr.IsValid() {
return fmt.Errorf("invalid address: %s", addr)
}
diskConf["http"] = yobj{
"address": addr.String(),
"session_ttl": timeutil.Duration{Duration: time.Duration(sessionTTL) * time.Hour}.String(),
}
delete(diskConf, "bind_host")
delete(diskConf, "bind_port")
delete(diskConf, "web_session_ttl")
return nil
}
// upgradeSchema23to24 performs the following changes:
//
// # BEFORE:
// 'log_file': ""
// 'log_max_backups': 0
// 'log_max_size': 100
// 'log_max_age': 3
// 'log_compress': false
// 'log_localtime': false
// 'verbose': false
//
// # AFTER:
// 'log':
// 'file': ""
// 'max_backups': 0
// 'max_size': 100
// 'max_age': 3
// 'compress': false
// 'local_time': false
// 'verbose': false
func upgradeSchema23to24(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 23 to 24")
diskConf["schema_version"] = 24
logObj := yobj{}
err = coalesceError(
moveField[string](diskConf, logObj, "log_file", "file"),
moveField[int](diskConf, logObj, "log_max_backups", "max_backups"),
moveField[int](diskConf, logObj, "log_max_size", "max_size"),
moveField[int](diskConf, logObj, "log_max_age", "max_age"),
moveField[bool](diskConf, logObj, "log_compress", "compress"),
moveField[bool](diskConf, logObj, "log_localtime", "local_time"),
moveField[bool](diskConf, logObj, "verbose", "verbose"),
)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
if len(logObj) != 0 {
diskConf["log"] = logObj
}
delete(diskConf, "log_file")
delete(diskConf, "log_max_backups")
delete(diskConf, "log_max_size")
delete(diskConf, "log_max_age")
delete(diskConf, "log_compress")
delete(diskConf, "log_localtime")
delete(diskConf, "verbose")
return nil
}
// moveField gets field value for key from diskConf, and then set this value
// in newConf for newKey.
func moveField[T any](diskConf, newConf yobj, key, newKey string) (err error) {
ok, newVal, err := fieldValue[T](diskConf, key)
if !ok {
return err
}
switch v := newVal.(type) {
case int, bool, string:
newConf[newKey] = v
default:
return fmt.Errorf("invalid type of %s: %T", key, newVal)
}
return nil
}
// fieldValue returns the value of type T for key in diskConf object.
func fieldValue[T any](diskConf yobj, key string) (ok bool, field any, err error) {
fieldVal, ok := diskConf[key]
if !ok {
return false, new(T), nil
}
f, ok := fieldVal.(T)
if !ok {
return false, nil, fmt.Errorf("unexpected type of %s: %T", key, fieldVal)
}
return true, f, nil
}
// coalesceError returns the first non-nil error. It is named after function
// COALESCE in SQL. If all errors are nil, it returns nil.
//
// TODO(a.garipov): Consider a similar helper to group errors together to show
// as many errors as possible.
//
// TODO(a.garipov): Think of ways to merge with [aghalg.Coalesce].
func coalesceError(errors ...error) (res error) {
for _, err := range errors {
if err != nil {
return err
}
}
return nil
}
// TODO(a.garipov): Replace with log.Output when we port it to our logging
// package.
func funcName() string {

View File

@@ -1253,3 +1253,129 @@ func TestUpgradeSchema21to22(t *testing.T) {
})
}
}
func TestUpgradeSchema22to23(t *testing.T) {
const newSchemaVer = 23
testCases := []struct {
in yobj
want yobj
name string
}{{
name: "empty",
in: yobj{},
want: yobj{
"schema_version": newSchemaVer,
},
}, {
name: "ok",
in: yobj{
"bind_host": "1.2.3.4",
"bind_port": 8081,
"web_session_ttl": 720,
},
want: yobj{
"http": yobj{
"address": "1.2.3.4:8081",
"session_ttl": "720h",
},
"schema_version": newSchemaVer,
},
}, {
name: "v6_address",
in: yobj{
"bind_host": "2001:db8::1",
"bind_port": 8081,
"web_session_ttl": 720,
},
want: yobj{
"http": yobj{
"address": "[2001:db8::1]:8081",
"session_ttl": "720h",
},
"schema_version": newSchemaVer,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema22to23(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema23to24(t *testing.T) {
const newSchemaVer = 24
testCases := []struct {
in yobj
want yobj
name string
wantErrMsg string
}{{
name: "empty",
in: yobj{},
want: yobj{
"schema_version": newSchemaVer,
},
wantErrMsg: "",
}, {
name: "ok",
in: yobj{
"log_file": "/test/path.log",
"log_max_backups": 1,
"log_max_size": 2,
"log_max_age": 3,
"log_compress": true,
"log_localtime": true,
"verbose": true,
},
want: yobj{
"log": yobj{
"file": "/test/path.log",
"max_backups": 1,
"max_size": 2,
"max_age": 3,
"compress": true,
"local_time": true,
"verbose": true,
},
"schema_version": newSchemaVer,
},
wantErrMsg: "",
}, {
name: "invalid",
in: yobj{
"log_file": "/test/path.log",
"log_max_backups": 1,
"log_max_size": 2,
"log_max_age": 3,
"log_compress": "",
"log_localtime": true,
"verbose": true,
},
want: yobj{
"log_file": "/test/path.log",
"log_max_backups": 1,
"log_max_size": 2,
"log_max_age": 3,
"log_compress": "",
"log_localtime": true,
"verbose": true,
"schema_version": newSchemaVer,
},
wantErrMsg: "unexpected type of log_compress: string",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema23to24(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.want, tc.in)
})
}
}

View File

@@ -119,7 +119,9 @@ func webCheckPortAvailable(port int) (ok bool) {
return true
}
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
addrPort := netip.AddrPortFrom(config.HTTPConfig.Address.Addr(), uint16(port))
return aghnet.CheckPort("tcp", addrPort) == nil
}
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server

View File

@@ -23,4 +23,5 @@ http:
secure_addresses: []
timeout: 5s
force_https: true
verbose: true
log:
verbose: true

View File

@@ -24,6 +24,7 @@ enough.
### Fixed
- `--check-config` breaking the configuration file ([#4067]).
- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).
- The order of `-v/--verbose` and `--version` being significant ([#2893]).
@@ -36,4 +37,5 @@ enough.
[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598
[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893
[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902
[#4067]: https://github.com/AdguardTeam/AdGuardHome/issues/4067
[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676

View File

@@ -16,11 +16,9 @@ import (
)
// Main is the entry point of AdGuard Home.
func Main(frontend fs.FS) {
func Main(embeddedFrontend fs.FS) {
start := time.Now()
// Initial Configuration
cmdName := os.Args[0]
opts, err := parseOptions(cmdName, os.Args[1:])
exitCode, needExit := processOptions(opts, cmdName, err)
@@ -39,9 +37,17 @@ func Main(frontend fs.FS) {
check(err)
}
// Web Service
frontend, err := frontendFromOpts(opts, embeddedFrontend)
check(err)
confMgr, err := configmgr.New(opts.confFile, frontend, start)
confMgrConf := &configmgr.Config{
Frontend: frontend,
WebAddr: opts.webAddr,
Start: start,
FileName: opts.confFile,
}
confMgr, err := newConfigMgr(confMgrConf)
check(err)
web := confMgr.Web()
@@ -53,9 +59,8 @@ func Main(frontend fs.FS) {
check(err)
sigHdlr := newSignalHandler(
opts.confFile,
frontend,
start,
confMgrConf,
opts.pidFile,
web,
dns,
)
@@ -73,6 +78,15 @@ func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), defaultTimeout)
}
// newConfigMgr returns a new configuration manager using defaultTimeout as the
// context timeout.
func newConfigMgr(c *configmgr.Config) (m *configmgr.Manager, err error) {
ctx, cancel := ctxWithDefaultTimeout()
defer cancel()
return configmgr.New(ctx, c)
}
// check is a simple error-checking helper. It must only be used within Main.
func check(err error) {
if err != nil {

View File

@@ -1,14 +1,18 @@
package cmd
import (
"encoding"
"flag"
"fmt"
"io"
"io/fs"
"net/netip"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/exp/slices"
)
@@ -25,8 +29,6 @@ type options struct {
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:
@@ -49,15 +51,12 @@ type options struct {
// 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
// webAddr contains the address on which to serve the web UI.
webAddr netip.AddrPort
// checkConfig, if true, instructs AdGuard Home to check the configuration
// file and exit with a corresponding exit code.
//
// TODO(a.garipov): Use.
// file, optionally print an error message to stdout, and exit with a
// corresponding exit code.
checkConfig bool
// disableUpdate, if true, prevents AdGuard Home from automatically checking
@@ -103,7 +102,7 @@ const (
pidFileIdx
serviceActionIdx
workDirIdx
webAddrsIdx
webAddrIdx
checkConfigIdx
disableUpdateIdx
glinetModeIdx
@@ -172,18 +171,17 @@ var commandLineOptions = []*commandLineOption{
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",
webAddrIdx: {
defaultValue: netip.AddrPort{},
description: `Address to serve the web UI on, in the host:port format.`,
long: "web-addr",
short: "",
valueType: "host:port",
},
checkConfigIdx: {
defaultValue: false,
description: "Check configuration and quit.",
description: "Check configuration, print errors to stdout, and quit.",
long: "check-config",
short: "",
valueType: "",
@@ -258,7 +256,7 @@ func parseOptions(cmdName string, args []string) (opts *options, err error) {
pidFileIdx: &opts.pidFile,
serviceActionIdx: &opts.serviceAction,
workDirIdx: &opts.workDir,
webAddrsIdx: &opts.webAddrs,
webAddrIdx: &opts.webAddr,
checkConfigIdx: &opts.checkConfig,
disableUpdateIdx: &opts.disableUpdate,
glinetModeIdx: &opts.glinetMode,
@@ -291,23 +289,16 @@ func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) {
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)
}
case encoding.TextUnmarshaler:
flags.TextVar(fieldPtr, o.long, o.defaultValue.(encoding.TextMarshaler), o.description)
if o.short != "" {
flags.TextVar(fieldPtr, o.short, o.defaultValue.(encoding.TextMarshaler), o.description)
}
default:
panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr))
}
@@ -380,13 +371,13 @@ func processOptions(
) (exitCode int, needExit bool) {
if parseErr != nil {
// Assume that usage has already been printed.
return 2, true
return statusArgumentError, true
}
if opts.help {
usage(cmdName, os.Stdout)
return 0, true
return statusSuccess, true
}
if opts.version {
@@ -396,8 +387,32 @@ func processOptions(
fmt.Printf("AdGuard Home %s\n", version.Version())
}
return 0, true
return statusSuccess, true
}
if opts.checkConfig {
err := configmgr.Validate(opts.confFile)
if err != nil {
_, _ = io.WriteString(os.Stdout, err.Error()+"\n")
return statusError, true
}
return statusSuccess, true
}
return 0, false
}
// frontendFromOpts returns the frontend to use based on the options.
func frontendFromOpts(opts *options, embeddedFrontend fs.FS) (frontend fs.FS, err error) {
const frontendSubdir = "build/static"
if opts.localFrontend {
log.Info("warning: using local frontend files")
return os.DirFS(frontendSubdir), nil
}
return fs.Sub(embeddedFrontend, frontendSubdir)
}

View File

@@ -1,30 +1,27 @@
package cmd
import (
"io/fs"
"os"
"time"
"strconv"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/golibs/log"
"github.com/google/renameio/maybe"
)
// signalHandler processes incoming signals and shuts services down.
type signalHandler struct {
// confMgrConf contains the configuration parameters for the configuration
// manager.
confMgrConf *configmgr.Config
// signal is the channel to which OS signals are sent.
signal chan os.Signal
// confFile is the path to the configuration file.
confFile string
// frontend is the filesystem with the frontend and other statically
// compiled files.
frontend fs.FS
// start is the time at which AdGuard Home has been started.
start time.Time
// pidFile is the path to the file where to store the PID, if any.
pidFile string
// services are the services that are shut down before application exiting.
services []agh.Service
@@ -34,6 +31,8 @@ type signalHandler struct {
func (h *signalHandler) handle() {
defer log.OnPanic("signalHandler.handle")
h.writePID()
for sig := range h.signal {
log.Info("sighdlr: received signal %q", sig)
@@ -41,6 +40,8 @@ func (h *signalHandler) handle() {
h.reconfigure()
} else if aghos.IsShutdownSignal(sig) {
status := h.shutdown()
h.removePID()
log.Info("sighdlr: exiting with status %d", status)
os.Exit(status)
@@ -63,7 +64,7 @@ func (h *signalHandler) reconfigure() {
// reconfigured without the full shutdown, and the error handling is
// currently not the best.
confMgr, err := configmgr.New(h.confFile, h.frontend, h.start)
confMgr, err := newConfigMgr(h.confMgrConf)
check(err)
web := confMgr.Web()
@@ -84,8 +85,9 @@ func (h *signalHandler) reconfigure() {
// Exit status constants.
const (
statusSuccess = 0
statusError = 1
statusSuccess = 0
statusError = 1
statusArgumentError = 2
)
// shutdown gracefully shuts down all services.
@@ -109,17 +111,15 @@ func (h *signalHandler) shutdown() (status int) {
// newSignalHandler returns a new signalHandler that shuts down svcs.
func newSignalHandler(
confFile string,
frontend fs.FS,
start time.Time,
confMgrConf *configmgr.Config,
pidFile string,
svcs ...agh.Service,
) (h *signalHandler) {
h = &signalHandler{
signal: make(chan os.Signal, 1),
confFile: confFile,
frontend: frontend,
start: start,
services: svcs,
confMgrConf: confMgrConf,
signal: make(chan os.Signal, 1),
pidFile: pidFile,
services: svcs,
}
aghos.NotifyShutdownSignal(h.signal)
@@ -127,3 +127,41 @@ func newSignalHandler(
return h
}
// writePID writes the PID to the file, if needed. Any errors are reported to
// log.
func (h *signalHandler) writePID() {
if h.pidFile == "" {
return
}
// Use 8, since most PIDs will fit.
data := make([]byte, 0, 8)
data = strconv.AppendInt(data, int64(os.Getpid()), 10)
data = append(data, '\n')
err := maybe.WriteFile(h.pidFile, data, 0o644)
if err != nil {
log.Error("sighdlr: writing pidfile: %s", err)
return
}
log.Debug("sighdlr: wrote pid to %q", h.pidFile)
}
// removePID removes the PID file, if any.
func (h *signalHandler) removePID() {
if h.pidFile == "" {
return
}
err := os.Remove(h.pidFile)
if err != nil {
log.Error("sighdlr: removing pidfile: %s", err)
return
}
log.Debug("sighdlr: removed pid at %q", h.pidFile)
}

View File

@@ -1,8 +1,10 @@
package configmgr
import (
"fmt"
"net/netip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
)
@@ -12,16 +14,49 @@ import (
type config struct {
DNS *dnsConfig `yaml:"dns"`
HTTP *httpConfig `yaml:"http"`
Log *logConfig `yaml:"log"`
// TODO(a.garipov): Use.
SchemaVersion int `yaml:"schema_version"`
// TODO(a.garipov): Use.
DebugPprof bool `yaml:"debug_pprof"`
Verbose bool `yaml:"verbose"`
}
const errNoConf errors.Error = "configuration not found"
// validate returns an error if the configuration structure is invalid.
func (c *config) validate() (err error) {
if c == nil {
return errNoConf
}
// TODO(a.garipov): Add more validations.
// Keep this in the same order as the fields in the config.
validators := []struct {
validate func() (err error)
name string
}{{
validate: c.DNS.validate,
name: "dns",
}, {
validate: c.HTTP.validate,
name: "http",
}, {
validate: c.Log.validate,
name: "log",
}}
for _, v := range validators {
err = v.validate()
if err != nil {
return fmt.Errorf("%s: %w", v.name, err)
}
}
return nil
}
// dnsConfig is the on-disk DNS configuration.
//
// TODO(a.garipov): Validate.
type dnsConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
BootstrapDNS []string `yaml:"bootstrap_dns"`
@@ -32,12 +67,57 @@ type dnsConfig struct {
UseDNS64 bool `yaml:"use_dns64"`
}
// httpConfig is the on-disk web API configuration.
// validate returns an error if the DNS configuration structure is invalid.
//
// TODO(a.garipov): Validate.
// TODO(a.garipov): Add more validations.
func (c *dnsConfig) validate() (err error) {
// TODO(a.garipov): Add more validations.
switch {
case c == nil:
return errNoConf
case c.UpstreamTimeout.Duration <= 0:
return newMustBePositiveError("upstream_timeout", c.UpstreamTimeout)
default:
return nil
}
}
// httpConfig is the on-disk web API configuration.
type httpConfig struct {
// TODO(a.garipov): Document the configuration change.
Addresses []netip.AddrPort `yaml:"addresses"`
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
Timeout timeutil.Duration `yaml:"timeout"`
ForceHTTPS bool `yaml:"force_https"`
}
// validate returns an error if the HTTP configuration structure is invalid.
//
// TODO(a.garipov): Add more validations.
func (c *httpConfig) validate() (err error) {
switch {
case c == nil:
return errNoConf
case c.Timeout.Duration <= 0:
return newMustBePositiveError("timeout", c.Timeout)
default:
return nil
}
}
// logConfig is the on-disk web API configuration.
type logConfig struct {
// TODO(a.garipov): Use.
Verbose bool `yaml:"verbose"`
}
// validate returns an error if the HTTP configuration structure is invalid.
//
// TODO(a.garipov): Add more validations.
func (c *logConfig) validate() (err error) {
if c == nil {
return errNoConf
}
return nil
}

View File

@@ -1,11 +1,14 @@
// Package configmgr defines the AdGuard Home on-disk configuration entities and
// configuration manager.
//
// TODO(a.garipov): Add tests.
package configmgr
import (
"context"
"fmt"
"io/fs"
"net/netip"
"os"
"sync"
"time"
@@ -14,6 +17,10 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/google/renameio/maybe"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
)
@@ -21,6 +28,8 @@ import (
// Manager handles full and partial changes in the configuration, persisting
// them to disk if necessary.
//
// TODO(a.garipov): Support missing configs and default values.
type Manager struct {
// updMu makes sure that at most one reconfiguration is performed at a time.
// updMu protects all fields below.
@@ -39,17 +48,69 @@ type Manager struct {
fileName string
}
// Validate returns an error if the configuration file with the given name does
// not exist or is invalid.
func Validate(fileName string) (err error) {
conf, err := read(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
// Don't wrap the error, because it's informative enough as is.
return conf.validate()
}
// Config contains the configuration parameters for the configuration manager.
type Config struct {
// Frontend is the filesystem with the frontend files.
Frontend fs.FS
// WebAddr is the initial or override address for the Web UI. It is not
// written to the configuration file.
WebAddr netip.AddrPort
// Start is the time of start of AdGuard Home.
Start time.Time
// FileName is the path to the configuration file.
FileName string
}
// New creates a new *Manager that persists changes to the file pointed to by
// fileName. It reads the configuration file and populates the service fields.
// start is the startup time of AdGuard Home.
func New(
fileName string,
frontend fs.FS,
start time.Time,
) (m *Manager, err error) {
// c.FileName. It reads the configuration file and populates the service
// fields. c must not be nil.
func New(ctx context.Context, c *Config) (m *Manager, err error) {
conf, err := read(c.FileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
err = conf.validate()
if err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
m = &Manager{
updMu: &sync.RWMutex{},
current: conf,
fileName: c.FileName,
}
err = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start)
if err != nil {
return nil, fmt.Errorf("creating config manager: %w", err)
}
return m, nil
}
// read reads and decodes configuration from the provided filename.
func read(fileName string) (conf *config, err error) {
defer func() { err = errors.Annotate(err, "reading config: %w") }()
conf := &config{}
conf = &config{}
f, err := os.Open(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
@@ -63,27 +124,7 @@ func New(
return nil, err
}
// TODO(a.garipov): Validate the configuration structure. Return an error
// if it's incorrect.
m = &Manager{
updMu: &sync.RWMutex{},
current: conf,
fileName: fileName,
}
// TODO(a.garipov): Get the context with the timeout from the arguments?
const assemblyTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
defer cancel()
err = m.assemble(ctx, conf, frontend, start)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return m, nil
return conf, nil
}
// assemble creates all services and puts them into the corresponding fields.
@@ -92,6 +133,7 @@ func (m *Manager) assemble(
ctx context.Context,
conf *config,
frontend fs.FS,
webAddr netip.AddrPort,
start time.Time,
) (err error) {
dnsConf := &dnssvc.Config{
@@ -116,6 +158,7 @@ func (m *Manager) assemble(
Start: start,
Addresses: conf.HTTP.Addresses,
SecureAddresses: conf.HTTP.SecureAddresses,
OverrideAddress: webAddr,
Timeout: conf.HTTP.Timeout.Duration,
ForceHTTPS: conf.HTTP.ForceHTTPS,
}
@@ -128,6 +171,23 @@ func (m *Manager) assemble(
return nil
}
// write writes the current configuration to disk.
func (m *Manager) write() (err error) {
b, err := yaml.Marshal(m.current)
if err != nil {
return fmt.Errorf("encoding: %w", err)
}
err = maybe.WriteFile(m.fileName, b, 0o644)
if err != nil {
return fmt.Errorf("writing: %w", err)
}
log.Info("configmgr: written to %q", m.fileName)
return nil
}
// DNS returns the current DNS service. It is safe for concurrent use.
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
m.updMu.RLock()
@@ -150,7 +210,9 @@ func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
return fmt.Errorf("reassembling dnssvc: %w", err)
}
return nil
m.updateCurrentDNS(c)
return m.write()
}
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
@@ -172,6 +234,17 @@ func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
return nil
}
// updateCurrentDNS updates the DNS configuration in the current config.
func (m *Manager) updateCurrentDNS(c *dnssvc.Config) {
m.current.DNS.Addresses = slices.Clone(c.Addresses)
m.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers)
m.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers)
m.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes)
m.current.DNS.UpstreamTimeout = timeutil.Duration{Duration: c.UpstreamTimeout}
m.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6
m.current.DNS.UseDNS64 = c.UseDNS64
}
// Web returns the current web service. It is safe for concurrent use.
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
m.updMu.RLock()
@@ -194,7 +267,9 @@ func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
return fmt.Errorf("reassembling websvc: %w", err)
}
return nil
m.updateCurrentWeb(c)
return m.write()
}
// updateWeb recreates the web service. m.upd is expected to be locked.
@@ -213,3 +288,11 @@ func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
return nil
}
// updateCurrentWeb updates the web configuration in the current config.
func (m *Manager) updateCurrentWeb(c *websvc.Config) {
m.current.HTTP.Addresses = slices.Clone(c.Addresses)
m.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses)
m.current.HTTP.Timeout = timeutil.Duration{Duration: c.Timeout}
m.current.HTTP.ForceHTTPS = c.ForceHTTPS
}

View File

@@ -0,0 +1,27 @@
package configmgr
import (
"fmt"
"github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/constraints"
)
// numberOrDuration is the constraint for integer types along with
// timeutil.Duration.
type numberOrDuration interface {
constraints.Integer | timeutil.Duration
}
// newMustBePositiveError returns an error about the value that must be positive
// but isn't. prop is the name of the property to mention in the error message.
//
// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS
// as well.
func newMustBePositiveError[T numberOrDuration](prop string, v T) (err error) {
if s, ok := any(v).(fmt.Stringer); ok {
return fmt.Errorf("%s must be positive, got %s", prop, s)
}
return fmt.Errorf("%s must be positive, got %d", prop, v)
}

View File

@@ -6,13 +6,13 @@ import (
"sync/atomic"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/testutil/fakenet"
"github.com/stretchr/testify/assert"
)
func TestWaitListener_Accept(t *testing.T) {
var accepted atomic.Bool
var l net.Listener = &aghtest.Listener{
var l net.Listener = &fakenet.Listener{
OnAccept: func() (conn net.Conn, err error) {
accepted.Store(true)

View File

@@ -51,6 +51,10 @@ type Config struct {
// Start is the time of start of AdGuard Home.
Start time.Time
// OverrideAddress is the initial or override address for the HTTP API. If
// set, it is used instead of [Addresses] and [SecureAddresses].
OverrideAddress netip.AddrPort
// Addresses are the addresses on which to serve the plain HTTP API.
Addresses []netip.AddrPort
@@ -71,13 +75,14 @@ type Config struct {
// Service is the AdGuard Home web service. A nil *Service is a valid
// [agh.Service] that does nothing.
type Service struct {
confMgr ConfigManager
frontend fs.FS
tls *tls.Config
start time.Time
servers []*http.Server
timeout time.Duration
forceHTTPS bool
confMgr ConfigManager
frontend fs.FS
tls *tls.Config
start time.Time
overrideAddr netip.AddrPort
servers []*http.Server
timeout time.Duration
forceHTTPS bool
}
// New returns a new properly initialized *Service. If c is nil, svc is a nil
@@ -91,54 +96,60 @@ func New(c *Config) (svc *Service, err error) {
return nil, nil
}
frontend, err := fs.Sub(c.Frontend, "build/static")
if err != nil {
return nil, fmt.Errorf("frontend fs: %w", err)
}
svc = &Service{
confMgr: c.ConfigManager,
frontend: frontend,
tls: c.TLS,
start: c.Start,
timeout: c.Timeout,
forceHTTPS: c.ForceHTTPS,
confMgr: c.ConfigManager,
frontend: c.Frontend,
tls: c.TLS,
start: c.Start,
overrideAddr: c.OverrideAddress,
timeout: c.Timeout,
forceHTTPS: c.ForceHTTPS,
}
mux := newMux(svc)
for _, a := range c.Addresses {
addr := a.String()
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
svc.servers = append(svc.servers, &http.Server{
Addr: addr,
Handler: mux,
ErrorLog: errLog,
ReadTimeout: c.Timeout,
WriteTimeout: c.Timeout,
IdleTimeout: c.Timeout,
ReadHeaderTimeout: c.Timeout,
})
}
if svc.overrideAddr != (netip.AddrPort{}) {
svc.servers = []*http.Server{newSrv(svc.overrideAddr, nil, mux, c.Timeout)}
} else {
for _, a := range c.Addresses {
svc.servers = append(svc.servers, newSrv(a, nil, mux, c.Timeout))
}
for _, a := range c.SecureAddresses {
addr := a.String()
errLog := log.StdLog("websvc: https: "+addr, log.ERROR)
svc.servers = append(svc.servers, &http.Server{
Addr: addr,
Handler: mux,
TLSConfig: c.TLS,
ErrorLog: errLog,
ReadTimeout: c.Timeout,
WriteTimeout: c.Timeout,
IdleTimeout: c.Timeout,
ReadHeaderTimeout: c.Timeout,
})
for _, a := range c.SecureAddresses {
svc.servers = append(svc.servers, newSrv(a, c.TLS, mux, c.Timeout))
}
}
return svc, nil
}
// newSrv returns a new *http.Server with the given parameters.
func newSrv(
addr netip.AddrPort,
tlsConf *tls.Config,
h http.Handler,
timeout time.Duration,
) (srv *http.Server) {
addrStr := addr.String()
srv = &http.Server{
Addr: addrStr,
Handler: h,
TLSConfig: tlsConf,
ReadTimeout: timeout,
WriteTimeout: timeout,
IdleTimeout: timeout,
ReadHeaderTimeout: timeout,
}
if tlsConf == nil {
srv.ErrorLog = log.StdLog("websvc: plain http: "+addrStr, log.ERROR)
} else {
srv.ErrorLog = log.StdLog("websvc: https: "+addrStr, log.ERROR)
}
return srv
}
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
// service.
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
@@ -205,23 +216,23 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
// ":0" addresses, addrs will not return the actual bound ports until Start is
// finished.
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
for _, srv := range svc.servers {
addrPort, err := netip.ParseAddrPort(srv.Addr)
if err != nil {
// Technically shouldn't happen, since all servers must have a valid
// address.
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
}
if svc.overrideAddr != (netip.AddrPort{}) {
return []netip.AddrPort{svc.overrideAddr}, nil
}
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
// relying only on the nilness of TLSConfig, check the length of the
for _, srv := range svc.servers {
// Use MustParseAddrPort, since no errors should technically happen
// here, because all servers must have a valid address.
addrPort := netip.MustParseAddrPort(srv.Addr)
// [srv.Serve] will set TLSConfig to an almost empty value, so, instead
// of relying only on the nilness of TLSConfig, check the length of the
// certificates field as well.
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
addrs = append(addrs, addrPort)
} else {
secureAddrs = append(secureAddrs, addrPort)
}
}
return addrs, secureAddrs

132
internal/rdns/rdns.go Normal file
View File

@@ -0,0 +1,132 @@
// Package rdns processes reverse DNS lookup queries.
package rdns
import (
"net/netip"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/bluele/gcache"
)
// Interface processes rDNS queries.
type Interface interface {
// Process makes rDNS request and returns domain name. changed indicates
// that domain name was updated since last request.
Process(ip netip.Addr) (host string, changed bool)
}
// Empty is an empty [Inteface] implementation which does nothing.
type Empty struct{}
// type check
var _ Interface = (*Empty)(nil)
// Process implements the [Interface] interface for Empty.
func (Empty) Process(_ netip.Addr) (host string, changed bool) {
return "", false
}
// Exchanger is a resolver for clients' addresses.
type Exchanger interface {
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
// or as external.
Exchange(ip netip.Addr) (host string, err error)
}
// Config is the configuration structure for Default.
type Config struct {
// Exchanger resolves IP addresses to domain names.
Exchanger Exchanger
// CacheSize is the maximum size of the cache. It must be greater than
// zero.
CacheSize int
// CacheTTL is the Time to Live duration for cached IP addresses.
CacheTTL time.Duration
}
// Default is the default rDNS query 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
// exchanger resolves IP addresses to domain names.
exchanger Exchanger
// cacheTTL is the Time to Live duration for cached IP addresses.
cacheTTL time.Duration
}
// New returns a new default rDNS query processor. conf must not be nil.
func New(conf *Config) (r *Default) {
return &Default{
cache: gcache.New(conf.CacheSize).LRU().Build(),
exchanger: conf.Exchanger,
cacheTTL: conf.CacheTTL,
}
}
// type check
var _ Interface = (*Default)(nil)
// Process implements the [Interface] interface for Default.
func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
fromCache, expired := r.findInCache(ip)
if !expired {
return fromCache, false
}
host, err := r.exchanger.Exchange(ip)
if err != nil {
log.Debug("rdns: resolving %q: %s", ip, err)
}
item := &cacheItem{
expiry: time.Now().Add(r.cacheTTL),
host: host,
}
err = r.cache.Set(ip, item)
if err != nil {
log.Debug("rdns: cache: adding item %q: %s", ip, err)
}
return host, fromCache == "" || host != fromCache
}
// findInCache finds domain name in the cache. expired is true if host is not
// valid anymore.
func (r *Default) findInCache(ip netip.Addr) (host string, expired bool) {
val, err := r.cache.Get(ip)
if err != nil {
if !errors.Is(err, gcache.KeyNotFoundError) {
log.Debug("rdns: cache: retrieving %q: %s", ip, err)
}
return "", true
}
item, ok := val.(*cacheItem)
if !ok {
log.Debug("rdns: cache: %q bad type %T", ip, val)
return "", true
}
return item.host, time.Now().After(item.expiry)
}
// 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
// host is the domain name of a runtime client.
host string
}

105
internal/rdns/rdns_test.go Normal file
View File

@@ -0,0 +1,105 @@
package rdns_test
import (
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/golibs/netutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fakeRDNSExchanger is a mock [rdns.Exchanger] implementation for tests.
type fakeRDNSExchanger struct {
OnExchange func(ip netip.Addr) (host string, err error)
}
// type check
var _ rdns.Exchanger = (*fakeRDNSExchanger)(nil)
// Exchange implements [rdns.Exchanger] interface for *fakeRDNSExchanger.
func (e *fakeRDNSExchanger) Exchange(ip netip.Addr) (host string, err error) {
return e.OnExchange(ip)
}
func TestDefault_Process(t *testing.T) {
ip1 := netip.MustParseAddr("1.2.3.4")
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
require.NoError(t, err)
ip2 := netip.MustParseAddr("4.3.2.1")
revAddr2, err := netutil.IPToReversedAddr(ip2.AsSlice())
require.NoError(t, err)
localIP := netip.MustParseAddr("192.168.0.1")
localRevAddr1, err := netutil.IPToReversedAddr(localIP.AsSlice())
require.NoError(t, err)
config := &rdns.Config{
CacheSize: 100,
CacheTTL: time.Hour,
}
testCases := []struct {
name string
addr netip.Addr
want string
}{{
name: "first",
addr: ip1,
want: revAddr1,
}, {
name: "second",
addr: ip2,
want: revAddr2,
}, {
name: "empty",
addr: netip.MustParseAddr("0.0.0.0"),
want: "",
}, {
name: "private",
addr: localIP,
want: localRevAddr1,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hit := 0
onExchange := func(ip netip.Addr) (host string, err error) {
hit++
switch ip {
case ip1:
return revAddr1, nil
case ip2:
return revAddr2, nil
case localIP:
return localRevAddr1, nil
default:
return "", nil
}
}
exchanger := &fakeRDNSExchanger{
OnExchange: onExchange,
}
config.Exchanger = exchanger
r := rdns.New(config)
got, changed := r.Process(tc.addr)
require.True(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
// From cache.
got, changed = r.Process(tc.addr)
require.False(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
})
}
}

View File

@@ -10,9 +10,10 @@ require (
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.16.0
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
golang.org/x/tools v0.11.0
golang.org/x/vuln v0.2.0
// TODO(a.garipov): Return to tagged releases once a new one appears.
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee
mvdan.cc/gofumpt v0.5.0
mvdan.cc/unparam v0.0.0-20230610194454-9ea02bef9868
)
@@ -26,9 +27,9 @@ require (
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
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-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/sys v0.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,6 +1,5 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
@@ -53,21 +52,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df h1:jfUqBujZx2dktJVEmZpCkyngz7MWrVv1y9kLOqFNsqw=
golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22 h1:e8iSCQYXZ4EB6q3kIfy2fgPFTvDbozqzRe4OuIOyrL4=
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
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.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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.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.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
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=
@@ -83,8 +82,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
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.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/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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=
@@ -97,10 +96,10 @@ golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4X
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.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/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
golang.org/x/vuln v0.2.0 h1:Dlz47lW0pvPHU7tnb10S8vbMn9GnV2B6eyT7Tem5XBI=
golang.org/x/vuln v0.2.0/go.mod h1:V0eyhHwaAaHrt42J9bgrN6rd12f6GU4T0Lu0ex2wDg4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -108,8 +107,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw=
honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee h1:mpyvMqtlVZTwEv78QL3S2ZDTMHMO1fgNwr2kC7+K7oU=
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
mvdan.cc/unparam v0.0.0-20230610194454-9ea02bef9868 h1:F4Q7pXcrU9UiU1fq0ZWqSOxKjNAteRuDr7JDk7uVLRQ=

View File

@@ -288,6 +288,16 @@ func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed
return wi, false
}
return w.requestInfo(ctx, ip, wi)
}
// requestInfo makes WHOIS request and returns WHOIS info. changed is false if
// received information is equal to cached.
func (w *Default) requestInfo(
ctx context.Context,
ip netip.Addr,
cached *Info,
) (wi *Info, changed bool) {
var info Info
defer func() {
@@ -311,12 +321,14 @@ func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed
Orgname: kv["orgname"],
}
changed = cached == nil || info != *cached
// Don't return an empty struct so that the frontend doesn't get confused.
if (info == Info{}) {
return nil, true
return nil, changed
}
return &info, wi == nil || info != *wi
return &info, changed
}
// findInCache finds Info in the cache. expired indicates that Info is valid.

View File

@@ -18,7 +18,7 @@ Run `make init` from the project root.
## `make/`: Makefile Scripts
## `make/`: Makefile scripts
The release channels are: `development` (the default), `edge`, `beta`, and
`release`. If verbosity levels aren't documented here, there are only two: `0`,
@@ -26,7 +26,7 @@ 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:
@@ -51,7 +51,7 @@ Optional environment:
### `build-release.sh`: Build A Release For All Platforms
### `build-release.sh`: Build a release for all platforms
Required environment:
@@ -101,7 +101,22 @@ Required environment:
### `go-build.sh`: Build The Backend
### `go-bench.sh`: Run backend benchmarks
Optional environment:
* `GO`: set an alternative name for the Go compiler.
* `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 and environment.
The default value is `0`, don't be verbose.
### `go-build.sh`: Build the backend
Optional environment:
@@ -135,7 +150,7 @@ Required environment:
### `go-deps.sh`: Install Backend Dependencies
### `go-deps.sh`: Install backend dependencies
Optional environment:
@@ -147,7 +162,25 @@ Optional environment:
### `go-lint.sh`: Run Backend Static Analyzers
### `go-fuzz.sh`: Run backend fuzz tests
Optional environment:
* `GO`: set an alternative name for the Go compiler.
* `FUZZTIME_FLAGS`: set fuss flags for tests. The default value is
`--fuzztime=20s`.
* `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 and environment.
The default value is `0`, don't be verbose.
### `go-lint.sh`: Run backend static analyzers
Don't forget to run `make go-tools` once first!
@@ -163,7 +196,7 @@ Optional environment:
### `go-test.sh`: Run Backend Tests
### `go-test.sh`: Run backend tests
Optional environment:
@@ -173,7 +206,7 @@ Optional environment:
`1`, use the race detector.
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
`--timeout 30s`.
`--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
@@ -181,7 +214,7 @@ Optional environment:
### `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

View File

@@ -62,21 +62,22 @@ readonly docker_output
case "$channel"
in
('release')
docker_version_tag="${docker_image_name}:${version}"
docker_channel_tag="${docker_image_name}:latest"
docker_version_tag="--tag=${docker_image_name}:${version}"
docker_channel_tag="--tag=${docker_image_name}:latest"
;;
('beta')
docker_version_tag="${docker_image_name}:${version}"
docker_channel_tag="${docker_image_name}:beta"
docker_version_tag="--tag=${docker_image_name}:${version}"
docker_channel_tag="--tag=${docker_image_name}:beta"
;;
('edge')
# Don't set the version tag when pushing to the edge channel.
docker_version_tag="${docker_image_name}:edge"
docker_channel_tag="${docker_image_name}"
# Set the version tag to an empty string when pushing to the edge channel.
docker_version_tag=''
docker_channel_tag="--tag=${docker_image_name}:edge"
;;
('development')
docker_version_tag="${docker_image_name}"
docker_channel_tag="${docker_image_name}"
# Set both tags to an empty string for development builds.
docker_version_tag=''
docker_channel_tag=''
;;
(*)
echo "invalid channel '$channel', supported values are\
@@ -106,18 +107,11 @@ cp "${dist_dir}/AdGuardHome_linux_arm_7/AdGuardHome/AdGuardHome"\
cp "${dist_dir}/AdGuardHome_linux_ppc64le/AdGuardHome/AdGuardHome"\
"${dist_docker}/AdGuardHome_linux_ppc64le_"
# Copy the helper scripts. See file docker/Dockerfile.
dist_docker_scripts="${dist_docker}/scripts"
readonly dist_docker_scripts
mkdir -p "$dist_docker_scripts"
cp "./docker/dns-bind.awk"\
"${dist_docker_scripts}/dns-bind.awk"
cp "./docker/web-bind.awk"\
"${dist_docker_scripts}/web-bind.awk"
cp "./docker/healthcheck.sh"\
"${dist_docker_scripts}/healthcheck.sh"
# Don't use quotes with $docker_version_tag and $docker_channel_tag, because we
# want word splitting and or an empty space if tags are empty.
#
# TODO(a.garipov): Once flag --tag of docker buildx build supports commas, use
# them instead.
$sudo_cmd docker\
"$debug_flags"\
buildx build\
@@ -127,7 +121,7 @@ $sudo_cmd docker\
--build-arg VERSION="$version"\
--output "$docker_output"\
--platform "$docker_platforms"\
--tag="$docker_version_tag"\
--tag="$docker_channel_tag"\
$docker_version_tag\
$docker_channel_tag\
-f ./docker/Dockerfile\
.

55
scripts/make/go-bench.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
readonly verbose
# Verbosity levels:
# 0 = Don't print anything except for errors.
# 1 = Print commands, but not nested commands.
# 2 = Print everything.
if [ "$verbose" -gt '1' ]
then
set -x
v_flags='-v=1'
x_flags='-x=1'
elif [ "$verbose" -gt '0' ]
then
set -x
v_flags='-v=1'
x_flags='-x=0'
else
set +x
v_flags='-v=0'
x_flags='-x=0'
fi
readonly v_flags x_flags
set -e -f -u
if [ "${RACE:-1}" -eq '0' ]
then
race_flags='--race=0'
else
race_flags='--race=1'
fi
readonly race_flags
go="${GO:-go}"
count_flags='--count=1'
shuffle_flags='--shuffle=on'
timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}"
readonly go count_flags shuffle_flags timeout_flags
"$go" test\
"$count_flags"\
"$shuffle_flags"\
"$race_flags"\
"$timeout_flags"\
"$x_flags"\
"$v_flags"\
--bench='.'\
--benchmem\
--benchtime=1s\
--run='^$'\
./...

58
scripts/make/go-fuzz.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
readonly verbose
# Verbosity levels:
# 0 = Don't print anything except for errors.
# 1 = Print commands, but not nested commands.
# 2 = Print everything.
if [ "$verbose" -gt '1' ]
then
set -x
v_flags='-v=1'
x_flags='-x=1'
elif [ "$verbose" -gt '0' ]
then
set -x
v_flags='-v=1'
x_flags='-x=0'
else
set +x
v_flags='-v=0'
x_flags='-x=0'
fi
readonly v_flags x_flags
set -e -f -u
if [ "${RACE:-1}" -eq '0' ]
then
race_flags='--race=0'
else
race_flags='--race=1'
fi
readonly race_flags
go="${GO:-go}"
count_flags='--count=1'
shuffle_flags='--shuffle=on'
timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}"
fuzztime_flags="${FUZZTIME_FLAGS:---fuzztime=20s}"
readonly go count_flags shuffle_flags timeout_flags fuzztime_flags
# TODO(a.garipov): File an issue about using --fuzz with multiple packages.
"$go" test\
"$count_flags"\
"$shuffle_flags"\
"$race_flags"\
"$timeout_flags"\
"$x_flags"\
"$v_flags"\
"$fuzztime_flags"\
--fuzz='.'\
--run='^$'\
./internal/filtering/rulelist/\
;

View File

@@ -176,9 +176,13 @@ run_linter gocognit --over 10\
./internal/aghchan/\
./internal/aghhttp/\
./internal/aghio/\
./internal/tools/\
./internal/filtering/hashprefix/\
./internal/filtering/rulelist/\
./internal/next/\
./internal/rdns/\
./internal/tools/\
./internal/version/\
./internal/whois/\
;
run_linter ineffassign ./...
@@ -197,11 +201,29 @@ run_linter nilness ./...
run_linter -e shadow --strict ./...
# TODO(a.garipov): Enable in v0.108.0.
# run_linter gosec --quiet ./...
# TODO(a.garipov): Enable for all.
run_linter gosec --quiet\
./internal/aghalg\
./internal/aghchan\
./internal/aghhttp\
./internal/aghio\
./internal/aghnet\
./internal/aghos\
./internal/aghtest\
./internal/dhcpd\
./internal/dhcpsvc\
./internal/dnsforward\
./internal/filtering/hashprefix/\
./internal/filtering/rulelist/\
./internal/next\
./internal/schedule\
./internal/stats\
./internal/tools\
./internal/version\
./internal/whois\
;
# TODO(a.garipov): Enable --blank?
run_linter errcheck --asserts ./...
run_linter errcheck ./...
staticcheck_matrix='
darwin: GOOS=darwin

View File

@@ -1,7 +1,20 @@
#!/bin/sh
conf_file="${SNAP_DATA}/AdGuardHome.yaml"
readonly conf_file
if ! [ -f "$conf_file" ]
then
xdg-open 'http://localhost:3000'
exit
fi
# Get the admin interface port from the configuration.
bind_port="$( grep -e 'bind_port' "${SNAP_DATA}/AdGuardHome.yaml" | awk -F ' ' '{print $2}' )"
awk_prog='/^[^[:space:]]/ { is_http = /^http:/ };/^[[:space:]]+address:/ { if (is_http) print $2 }'
readonly awk_prog
bind_port="$( awk "$awk_prog" "$conf_file" | awk -F ':' '{print $NF}' )"
readonly bind_port
if [ "$bind_port" = '' ]