From 0cce420261b1e207e558f884d6c73cbec9d55f60 Mon Sep 17 00:00:00 2001
From: Ainar Garipov <a.garipov@adguard.com>
Date: Mon, 3 Oct 2022 18:08:05 +0300
Subject: [PATCH] Pull request: 3955-doh3

Updates #3955.

Squashed commit of the following:

commit acfd5ccc29ff03dfae1e51e52649acdf05042d9f
Merge: caeac6e5 61bd217e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 18:00:37 2022 +0300

    Merge branch 'master' into 3955-doh3

commit caeac6e5401bcaa95bba8d2b84a943b6c9a5898a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 17:54:16 2022 +0300

    all: fix server closing; imp docs

commit 87396141ff49d48ae54b4184559070e7885bccc7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 17:33:39 2022 +0300

    all: add doh3 support
---
 CHANGELOG.md                     |  16 ++++
 go.mod                           |  16 ++--
 go.sum                           |  35 +++++----
 internal/dnsforward/access.go    |  10 +--
 internal/dnsforward/config.go    |  39 ++++++++--
 internal/dnsforward/filter.go    |   2 +-
 internal/dnsforward/http.go      |  44 +++++------
 internal/dnsforward/http_test.go |   4 +-
 internal/home/clients.go         |   5 +-
 internal/home/config.go          |  13 ++++
 internal/home/control.go         |  14 +---
 internal/home/controlinstall.go  |  58 ++++++++------
 internal/home/dns.go             |   7 +-
 internal/home/home.go            |   4 +-
 internal/home/tls.go             |   4 +-
 internal/home/web.go             | 125 ++++++++++++++++++++++---------
 16 files changed, 255 insertions(+), 141 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f537699..c0da6c73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,12 +22,28 @@ and this project adheres to
   /control/stats_reset` requests) do not have a `Content-Type` header set on
   them ([#4970]).
 
+### Added
+
+#### Experimental HTTP/3 Support
+
+See [#3955] and the related issues for more details.  These features are still
+experimental and may break or change in the future.
+
+- DNS-over-HTTP/3 DNS and web UI client request support.  This feature must be
+  explicitly enabled by setting the new property `dns.serve_http3` in the
+  configuration file to `true`.
+- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
+  file property `use_http3_upstreams` is set to `true`.
+- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
+  using the `h3://` scheme.
+
 ### Fixed
 
 - User-specific blocked services not applying correctly ([#4945], [#4982],
   [#4983]).
 - `only application/json is allowed` errors in various APIs ([#4970]).
 
+[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955
 [#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
 
 
diff --git a/go.mod b/go.mod
index bc090305..b7acd047 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.18
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.44.0
+	github.com/AdguardTeam/dnsproxy v0.45.2
 	github.com/AdguardTeam/golibs v0.10.9
 	github.com/AdguardTeam/urlfilter v0.16.0
 	github.com/NYTimes/gziphandler v1.1.1
@@ -18,7 +18,7 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
 	github.com/kardianos/service v1.2.1
-	github.com/lucas-clemente/quic-go v0.29.0
+	github.com/lucas-clemente/quic-go v0.29.1
 	github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
 	github.com/mdlayher/netlink v1.6.0
 	// TODO(a.garipov): This package is deprecated; find a new one or use
@@ -28,10 +28,10 @@ require (
 	github.com/stretchr/testify v1.8.0
 	github.com/ti-mo/netfilter v0.4.0
 	go.etcd.io/bbolt v1.3.6
-	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
-	golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
-	golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
-	golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77
+	golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
+	golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9
+	golang.org/x/net v0.0.0-20220927171203-f486391704dc
+	golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/yaml.v3 v3.0.1
 	howett.net/plist v1.0.0
@@ -43,10 +43,12 @@ require (
 	github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
 	github.com/ameshkov/dnsstamps v1.0.3 // indirect
 	github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
+	github.com/bluele/gcache v0.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
 	github.com/golang/mock v1.6.0 // indirect
 	github.com/josharian/native v1.0.0 // indirect
+	github.com/marten-seemann/qpack v0.2.1 // indirect
 	github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
 	github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
 	github.com/mdlayher/packet v1.0.0 // indirect
@@ -57,7 +59,7 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
-	golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect
 	golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/tools v0.1.12 // indirect
diff --git a/go.sum b/go.sum
index a2a4197c..458cb14d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
-github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
+github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
+github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
 github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
@@ -23,6 +23,8 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
 github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
+github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
+github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 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=
@@ -88,8 +90,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
-github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
+github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
+github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
+github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
+github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
 github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
 github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
 github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
@@ -125,6 +129,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -168,16 +173,16 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
-golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
-golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
+golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
+golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.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.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
-golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8=
+golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -189,6 +194,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -200,8 +206,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
-golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
+golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -224,6 +230,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -247,8 +254,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
-golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go
index 33c9a978..23ec1137 100644
--- a/internal/dnsforward/access.go
+++ b/internal/dnsforward/access.go
@@ -183,15 +183,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
 }
 
 func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
-	j := s.accessListJSON()
-
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewEncoder(w).Encode(j)
-	if err != nil {
-		aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
-
-		return
-	}
+	_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
 }
 
 // validateAccessSet checks the internal accessListJSON lists.  To search for
diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go
index 747767c4..f8e51ff0 100644
--- a/internal/dnsforward/config.go
+++ b/internal/dnsforward/config.go
@@ -201,6 +201,10 @@ type ServerConfig struct {
 	// Register an HTTP handler
 	HTTPRegister aghhttp.RegisterFunc
 
+	// LocalPTRResolvers is a slice of addresses to be used as upstreams for
+	// resolving PTR queries for local addresses.
+	LocalPTRResolvers []string
+
 	// ResolveClients signals if the RDNS should resolve clients' addresses.
 	ResolveClients bool
 
@@ -208,9 +212,12 @@ type ServerConfig struct {
 	// locally-served networks should be resolved via private PTR resolvers.
 	UsePrivateRDNS bool
 
-	// LocalPTRResolvers is a slice of addresses to be used as upstreams for
-	// resolving PTR queries for local addresses.
-	LocalPTRResolvers []string
+	// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
+	ServeHTTP3 bool
+
+	// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
+	// upstreams.
+	UseHTTP3Upstreams bool
 }
 
 // if any of ServerConfig values are zero, then default values from below are used
@@ -226,6 +233,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
 	conf = proxy.Config{
 		UDPListenAddr:          srvConf.UDPListenAddrs,
 		TCPListenAddr:          srvConf.TCPListenAddrs,
+		HTTP3:                  srvConf.ServeHTTP3,
 		Ratelimit:              int(srvConf.Ratelimit),
 		RatelimitWhitelist:     srvConf.RatelimitWhitelist,
 		RefuseAny:              srvConf.RefuseAny,
@@ -324,6 +332,20 @@ 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
@@ -353,12 +375,14 @@ func (s *Server) prepareUpstreamSettings() error {
 		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,
+			Bootstrap:    s.conf.BootstrapDNS,
+			Timeout:      s.conf.UpstreamTimeout,
+			HTTPVersions: httpVersions,
 		},
 	)
 	if err != nil {
@@ -371,8 +395,9 @@ func (s *Server) prepareUpstreamSettings() error {
 		uc, err = proxy.ParseUpstreamsConfig(
 			defaultDNS,
 			&upstream.Options{
-				Bootstrap: s.conf.BootstrapDNS,
-				Timeout:   s.conf.UpstreamTimeout,
+				Bootstrap:    s.conf.BootstrapDNS,
+				Timeout:      s.conf.UpstreamTimeout,
+				HTTPVersions: httpVersions,
 			},
 		)
 		if err != nil {
diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go
index 7dc8514e..6f64d35d 100644
--- a/internal/dnsforward/filter.go
+++ b/internal/dnsforward/filter.go
@@ -151,7 +151,7 @@ func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Set
 }
 
 // filterDNSResponse checks each resource record of the response's answer
-// section from pctx and returns a non-nil res if at least one of canonnical
+// section from pctx and returns a non-nil res if at least one of canonical
 // names or IP addresses in it matches the filtering rules.
 func (s *Server) filterDNSResponse(
 	pctx *proxy.DNSContext,
diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go
index 5df74fe8..91e31b70 100644
--- a/internal/dnsforward/http.go
+++ b/internal/dnsforward/http.go
@@ -112,13 +112,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 		DefautLocalPTRUpstreams: defLocalPTRUps,
 	}
 
-	w.Header().Set("Content-Type", "application/json")
-
-	if err = json.NewEncoder(w).Encode(resp); err != nil {
-		aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encoder: %s", err)
-
-		return
-	}
+	_ = aghhttp.WriteJSONResponse(w, r, resp)
 }
 
 func (req *jsonDNSConfig) checkBlockingMode() (err error) {
@@ -349,7 +343,10 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
 
 	conf, err = proxy.ParseUpstreamsConfig(
 		upstreams,
-		&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
+		&upstream.Options{
+			Bootstrap: []string{},
+			Timeout:   DefaultTimeout,
+		},
 	)
 	if err != nil {
 		return nil, err
@@ -412,7 +409,15 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
 	return nil
 }
 
-var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
+var protocols = []string{
+	"h3://",
+	"https://",
+	"quic://",
+	"sdns://",
+	"tcp://",
+	"tls://",
+	"udp://",
+}
 
 // validateUpstream returns an error if u alongside with domains is not a valid
 // upstream configuration.  useDefault is true if the upstream is
@@ -659,24 +664,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
 		result[host] = "OK"
 	}
 
-	jsonVal, err := json.Marshal(result)
-	if err != nil {
-		aghhttp.Error(
-			r,
-			w,
-			http.StatusInternalServerError,
-			"Unable to marshal status json: %s",
-			err,
-		)
-
-		return
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	_, err = w.Write(jsonVal)
-	if err != nil {
-		aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
-	}
+	_ = aghhttp.WriteJSONResponse(w, r, result)
 }
 
 // handleDoH is the DNS-over-HTTPs handler.
@@ -692,11 +680,13 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
 func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
 	if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
 		aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
+
 		return
 	}
 
 	if !s.IsRunning() {
 		aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
+
 		return
 	}
 
diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go
index 1ae39455..f591ac58 100644
--- a/internal/dnsforward/http_test.go
+++ b/internal/dnsforward/http_test.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
 	"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
 	"github.com/AdguardTeam/AdGuardHome/internal/filtering"
 	"github.com/AdguardTeam/golibs/netutil"
@@ -116,7 +117,8 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
 			s.conf = tc.conf()
 			s.handleGetConfig(w, nil)
 
-			assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
+			cType := w.Header().Get(aghhttp.HdrNameContentType)
+			assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
 			assert.JSONEq(t, string(caseWant), w.Body.String())
 		})
 	}
diff --git a/internal/home/clients.go b/internal/home/clients.go
index 7396e8c6..631d27ba 100644
--- a/internal/home/clients.go
+++ b/internal/home/clients.go
@@ -456,8 +456,9 @@ func (clients *clientsContainer) findUpstreams(
 	conf, err = proxy.ParseUpstreamsConfig(
 		upstreams,
 		&upstream.Options{
-			Bootstrap: config.DNS.BootstrapDNS,
-			Timeout:   config.DNS.UpstreamTimeout.Duration,
+			Bootstrap:    config.DNS.BootstrapDNS,
+			Timeout:      config.DNS.UpstreamTimeout.Duration,
+			HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
 		},
 	)
 	if err != nil {
diff --git a/internal/home/config.go b/internal/home/config.go
index ed337c86..598baf81 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -166,6 +166,19 @@ type dnsConfig struct {
 	// LocalPTRResolvers is the slice of addresses to be used as upstreams
 	// for PTR queries for locally-served networks.
 	LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
+
+	// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
+	//
+	// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
+	// experimental.
+	ServeHTTP3 bool `yaml:"serve_http3"`
+
+	// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
+	// upstreams.
+	//
+	// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
+	// experimental.
+	UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"`
 }
 
 type tlsConfigSettings struct {
diff --git a/internal/home/control.go b/internal/home/control.go
index 8abdb264..54d2efb1 100644
--- a/internal/home/control.go
+++ b/internal/home/control.go
@@ -1,7 +1,6 @@
 package home
 
 import (
-	"encoding/json"
 	"fmt"
 	"net"
 	"net/http"
@@ -155,19 +154,12 @@ type profileJSON struct {
 }
 
 func handleGetProfile(w http.ResponseWriter, r *http.Request) {
-	pj := profileJSON{}
 	u := Context.auth.getCurrentUser(r)
-
-	pj.Name = u.Name
-
-	data, err := json.Marshal(pj)
-	if err != nil {
-		aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
-
-		return
+	resp := &profileJSON{
+		Name: u.Name,
 	}
 
-	_, _ = w.Write(data)
+	_ = aghhttp.WriteJSONResponse(w, r, resp)
 }
 
 // ------------------------
diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go
index 78de64f7..7df8d320 100644
--- a/internal/home/controlinstall.go
+++ b/internal/home/controlinstall.go
@@ -21,6 +21,7 @@ import (
 	"github.com/AdguardTeam/AdGuardHome/internal/version"
 	"github.com/AdguardTeam/golibs/errors"
 	"github.com/AdguardTeam/golibs/log"
+	"github.com/lucas-clemente/quic-go/http3"
 )
 
 // getAddrsResponse is the response for /install/get_addresses endpoint.
@@ -328,6 +329,7 @@ func copyInstallSettings(dst, src *configuration) {
 // shutdownTimeout is the timeout for shutting HTTP server down operation.
 const shutdownTimeout = 5 * time.Second
 
+// shutdownSrv shuts srv down and prints error messages to the log.
 func shutdownSrv(ctx context.Context, srv *http.Server) {
 	defer log.OnPanic("")
 
@@ -336,13 +338,38 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
 	}
 
 	err := srv.Shutdown(ctx)
-	if err != nil {
-		const msgFmt = "shutting down http server %q: %s"
-		if errors.Is(err, context.Canceled) {
-			log.Debug(msgFmt, srv.Addr, err)
-		} else {
-			log.Error(msgFmt, srv.Addr, err)
-		}
+	if err == nil {
+		return
+	}
+
+	const msgFmt = "shutting down http server %q: %s"
+	if errors.Is(err, context.Canceled) {
+		log.Debug(msgFmt, srv.Addr, err)
+	} else {
+		log.Error(msgFmt, srv.Addr, err)
+	}
+}
+
+// shutdownSrv3 shuts srv down and prints error messages to the log.
+//
+// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].
+func shutdownSrv3(srv *http3.Server) {
+	defer log.OnPanic("")
+
+	if srv == nil {
+		return
+	}
+
+	err := srv.Close()
+	if err == nil {
+		return
+	}
+
+	const msgFmt = "shutting down http/3 server %q: %s"
+	if errors.Is(err, context.Canceled) {
+		log.Debug(msgFmt, srv.Addr, err)
+	} else {
+		log.Error(msgFmt, srv.Addr, err)
 	}
 }
 
@@ -545,16 +572,11 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
 
 	err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
 	if err != nil {
-		aghhttp.Error(
-			r,
-			w,
-			http.StatusBadRequest,
-			"Failed to encode 'check_config' JSON data: %s",
-			err,
-		)
+		aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err)
 
 		return
 	}
+
 	body := nonBetaReqBody.String()
 	r.Body = io.NopCloser(strings.NewReader(body))
 	r.ContentLength = int64(len(body))
@@ -622,13 +644,7 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques
 
 	err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
 	if err != nil {
-		aghhttp.Error(
-			r,
-			w,
-			http.StatusBadRequest,
-			"Failed to encode 'check_config' JSON data: %s",
-			err,
-		)
+		aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err)
 
 		return
 	}
diff --git a/internal/home/dns.go b/internal/home/dns.go
index c304f6cc..da462876 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -246,11 +246,14 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
 	newConf.FilterHandler = applyAdditionalFiltering
 	newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
 
-	newConf.ResolveClients = config.Clients.Sources.RDNS
-	newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
 	newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
 	newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
 
+	newConf.ResolveClients = config.Clients.Sources.RDNS
+	newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
+	newConf.ServeHTTP3 = dnsConf.ServeHTTP3
+	newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
+
 	return newConf, nil
 }
 
diff --git a/internal/home/home.go b/internal/home/home.go
index f917b6a5..42c44249 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -381,9 +381,11 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
 
 		clientFS:     clientFS,
 		clientBetaFS: clientBetaFS,
+
+		serveHTTP3: config.DNS.ServeHTTP3,
 	}
 
-	web = CreateWeb(&webConf)
+	web = newWeb(&webConf)
 	if web == nil {
 		return nil, fmt.Errorf("initializing web: %w", err)
 	}
diff --git a/internal/home/tls.go b/internal/home/tls.go
index 359a7a9d..a5089bd8 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	if !WebCheckPortAvailable(setts.PortHTTPS) {
+	if !webCheckPortAvailable(setts.PortHTTPS) {
 		aghhttp.Error(
 			r,
 			w,
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// TODO(e.burkov):  Investigate and perhaps check other ports.
-	if !WebCheckPortAvailable(data.PortHTTPS) {
+	if !webCheckPortAvailable(data.PortHTTPS) {
 		aghhttp.Error(
 			r,
 			w,
diff --git a/internal/home/web.go b/internal/home/web.go
index 5a26de59..3e248d80 100644
--- a/internal/home/web.go
+++ b/internal/home/web.go
@@ -16,6 +16,8 @@ import (
 	"github.com/AdguardTeam/golibs/log"
 	"github.com/AdguardTeam/golibs/netutil"
 	"github.com/NYTimes/gziphandler"
+	"github.com/lucas-clemente/quic-go"
+	"github.com/lucas-clemente/quic-go/http3"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2/h2c"
 )
@@ -53,40 +55,56 @@ type webConfig struct {
 	WriteTimeout time.Duration
 
 	firstRun bool
+
+	serveHTTP3 bool
 }
 
-// HTTPSServer - HTTPS Server
-type HTTPSServer struct {
-	server   *http.Server
-	cond     *sync.Cond
-	condLock sync.Mutex
-	shutdown bool // if TRUE, don't restart the server
-	enabled  bool
-	cert     tls.Certificate
+// httpsServer contains the data for the HTTPS server.
+type httpsServer struct {
+	// server is the pre-HTTP/3 HTTPS server.
+	server *http.Server
+	// server3 is the HTTP/3 HTTPS server.  If it is not nil,
+	// [httpsServer.server] must also be non-nil.
+	server3 *http3.Server
+
+	// TODO(a.garipov): Why is there a *sync.Cond here?  Remove.
+	cond       *sync.Cond
+	condLock   sync.Mutex
+	cert       tls.Certificate
+	inShutdown bool
+	enabled    bool
 }
 
-// Web - module object
+// Web is the web UI and API server.
 type Web struct {
-	conf        *webConfig
-	forceHTTPS  bool
-	httpServer  *http.Server // HTTP module
-	httpsServer HTTPSServer  // HTTPS module
+	conf *webConfig
 
-	// handlerBeta is the handler for new client.
-	handlerBeta http.Handler
-	// installerBeta is the pre-install handler for new client.
-	installerBeta http.Handler
+	// TODO(a.garipov): Refactor all these servers.
+	httpServer *http.Server
 
 	// httpServerBeta is a server for new client.
 	httpServerBeta *http.Server
+
+	// handlerBeta is the handler for new client.
+	handlerBeta http.Handler
+
+	// installerBeta is the pre-install handler for new client.
+	installerBeta http.Handler
+
+	// httpsServer is the server that handles HTTPS traffic.  If it is not nil,
+	// [Web.http3Server] must also not be nil.
+	httpsServer httpsServer
+
+	forceHTTPS bool
 }
 
-// CreateWeb - create module
-func CreateWeb(conf *webConfig) *Web {
-	log.Info("Initialize web module")
+// newWeb creates a new instance of the web UI and API server.
+func newWeb(conf *webConfig) (w *Web) {
+	log.Info("web: initializing")
 
-	w := Web{}
-	w.conf = conf
+	w = &Web{
+		conf: conf,
+	}
 
 	clientFS := http.FileServer(http.FS(conf.clientFS))
 	betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
@@ -108,12 +126,15 @@ func CreateWeb(conf *webConfig) *Web {
 	}
 
 	w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
-	return &w
+
+	return w
 }
 
-// WebCheckPortAvailable - check if port is available
-// BUT: if we are already using this port, no need
-func WebCheckPortAvailable(port int) bool {
+// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
+// available, unless the HTTPS server isn't active.
+//
+// TODO(a.garipov): Adapt for HTTP/3.
+func webCheckPortAvailable(port int) (ok bool) {
 	return Context.web.httpsServer.server != nil ||
 		aghnet.CheckPort("tcp", config.BindHost, port) == nil
 }
@@ -121,7 +142,7 @@ func WebCheckPortAvailable(port int) bool {
 // TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
 // if necessary.
 func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
-	log.Debug("Web: applying new TLS configuration")
+	log.Debug("web: applying new tls configuration")
 	web.conf.PortHTTPS = tlsConf.PortHTTPS
 	web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
 
@@ -143,6 +164,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
 		var cancel context.CancelFunc
 		ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
 		shutdownSrv(ctx, web.httpsServer.server)
+		shutdownSrv3(web.httpsServer.server3)
+
 		cancel()
 	}
 
@@ -160,7 +183,7 @@ func (web *Web) Start() {
 	go web.tlsServerLoop()
 
 	// this loop is used as an ability to change listening host and/or port
-	for !web.httpsServer.shutdown {
+	for !web.httpsServer.inShutdown {
 		printHTTPAddresses(aghhttp.SchemeHTTP)
 		errs := make(chan error, 2)
 
@@ -231,7 +254,7 @@ func (web *Web) Close(ctx context.Context) {
 	log.Info("stopping http server...")
 
 	web.httpsServer.cond.L.Lock()
-	web.httpsServer.shutdown = true
+	web.httpsServer.inShutdown = true
 	web.httpsServer.cond.L.Unlock()
 
 	var cancel context.CancelFunc
@@ -239,6 +262,7 @@ func (web *Web) Close(ctx context.Context) {
 	defer cancel()
 
 	shutdownSrv(ctx, web.httpsServer.server)
+	shutdownSrv3(web.httpsServer.server3)
 	shutdownSrv(ctx, web.httpServer)
 	shutdownSrv(ctx, web.httpServerBeta)
 
@@ -248,7 +272,7 @@ func (web *Web) Close(ctx context.Context) {
 func (web *Web) tlsServerLoop() {
 	for {
 		web.httpsServer.cond.L.Lock()
-		if web.httpsServer.shutdown {
+		if web.httpsServer.inShutdown {
 			web.httpsServer.cond.L.Unlock()
 			break
 		}
@@ -256,7 +280,7 @@ func (web *Web) tlsServerLoop() {
 		// this mechanism doesn't let us through until all conditions are met
 		for !web.httpsServer.enabled { // sleep until necessary data is supplied
 			web.httpsServer.cond.Wait()
-			if web.httpsServer.shutdown {
+			if web.httpsServer.inShutdown {
 				web.httpsServer.cond.L.Unlock()
 				return
 			}
@@ -264,11 +288,10 @@ func (web *Web) tlsServerLoop() {
 
 		web.httpsServer.cond.L.Unlock()
 
-		// prepare HTTPS server
-		address := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
+		addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
 		web.httpsServer.server = &http.Server{
 			ErrorLog: log.StdLog("web: https", log.DEBUG),
-			Addr:     address,
+			Addr:     addr,
 			TLSConfig: &tls.Config{
 				Certificates: []tls.Certificate{web.httpsServer.cert},
 				RootCAs:      Context.tlsRoots,
@@ -282,10 +305,40 @@ func (web *Web) tlsServerLoop() {
 		}
 
 		printHTTPAddresses(aghhttp.SchemeHTTPS)
+
+		if web.conf.serveHTTP3 {
+			go web.mustStartHTTP3(addr)
+		}
+
+		log.Debug("web: starting https server")
 		err := web.httpsServer.server.ListenAndServeTLS("", "")
-		if err != http.ErrServerClosed {
+		if !errors.Is(err, http.ErrServerClosed) {
 			cleanupAlways()
-			log.Fatal(err)
+			log.Fatalf("web: https: %s", err)
 		}
 	}
 }
+
+func (web *Web) mustStartHTTP3(address string) {
+	defer log.OnPanic("web: http3")
+
+	web.httpsServer.server3 = &http3.Server{
+		// TODO(a.garipov): See if there is a way to use the error log as
+		// well as timeouts here.
+		Addr: address,
+		TLSConfig: &tls.Config{
+			Certificates: []tls.Certificate{web.httpsServer.cert},
+			RootCAs:      Context.tlsRoots,
+			CipherSuites: aghtls.SaferCipherSuites(),
+			MinVersion:   tls.VersionTLS12,
+		},
+		Handler: withMiddlewares(Context.mux, limitRequestBody),
+	}
+
+	log.Debug("web: starting http/3 server")
+	err := web.httpsServer.server3.ListenAndServe()
+	if !errors.Is(err, quic.ErrServerClosed) {
+		cleanupAlways()
+		log.Fatalf("web: http3: %s", err)
+	}
+}