From c908eec5de2a04ab7b38b92e0e4eac9958909b19 Mon Sep 17 00:00:00 2001
From: Dimitry Kolyshev <dkolyshev@adguard.com>
Date: Tue, 12 Dec 2023 13:16:01 +0300
Subject: [PATCH] Pull request: home: http dns plain

Merge in DNS/adguard-home from AG-28194-plain-dns to master

Squashed commit of the following:

commit a033982b949217d46a8ea609f63198916f779a61
Merge: 03fc28211 79d7a1ef4
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Dec 12 12:07:39 2023 +0200

    Merge remote-tracking branch 'origin/master' into AG-28194-plain-dns

commit 03fc282119a6372fcb4ce17a5d89779ad84589f5
Merge: e31a65931 34a34dc05
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Dec 12 11:07:46 2023 +0200

    Merge remote-tracking branch 'origin/master' into AG-28194-plain-dns

    # Conflicts:
    #	CHANGELOG.md

commit e31a659312fffe0cd5f57710843c8a6818515502
Merge: 0b735eb42 7b5cce517
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Dec 11 11:09:07 2023 +0200

    Merge remote-tracking branch 'origin/master' into AG-28194-plain-dns

    # Conflicts:
    #	CHANGELOG.md

commit 0b735eb4261883961058aed562c1e72ad1a20915
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 15:22:27 2023 +0200

    Revert "safesearch: imp docs"

    This reverts commit bab6bf3467f8914a34413bbbcdc37e89ff0401a5.

commit bab6bf3467f8914a34413bbbcdc37e89ff0401a5
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 15:21:23 2023 +0200

    safesearch: imp docs

commit aa5e6e30e01bf947d645ac4a9578eeac09c92a19
Merge: 503888447 2b62901fe
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 14:48:13 2023 +0200

    Merge remote-tracking branch 'origin/AG-28194-plain-dns' into AG-28194-plain-dns

commit 503888447aaf30d48c3fb9a414e8a65beb1a4e23
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 14:47:23 2023 +0200

    home: imp code

commit 2b62901feb29c9613ae648fa5e83598157207a17
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Dec 8 11:55:25 2023 +0300

    client: add plain dns description

commit 3d51fc8ea1955e599953070a4b330dd4e2fd44bc
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 10:15:53 2023 +0200

    all: changelog

commit 59697b5f1ab049bd2259ffe42cef7223531ef7aa
Merge: 81a15d081 b668c04ea
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 8 10:11:59 2023 +0200

    Merge remote-tracking branch 'origin/master' into AG-28194-plain-dns

commit 81a15d0818b18f99e651311a8502082b4a539e4b
Author: Natalia Sokolova <n.sokolova@adguard.com>
Date:   Thu Dec 7 17:30:05 2023 +0300

    client/src/__locales/en.json edited online with Bitbucket

commit 0cf2f880fbd1592c02e6df42319cba357f0d7bc8
Author: Natalia Sokolova <n.sokolova@adguard.com>
Date:   Thu Dec 7 17:29:51 2023 +0300

    client/src/__locales/en.json edited online with Bitbucket

commit 2f32c59b8b1d764d060a69c35787566cf5210063
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Dec 7 13:14:04 2023 +0200

    home: imp code

commit 01e21a26bdd13c42c55c8ea3b5bbe84933bf0c04
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Dec 7 12:14:02 2023 +0200

    all: imp docs

commit b6beec6df7c2a9077ddce018656c701b7e875b53
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Dec 7 12:42:21 2023 +0300

    client: fix reset settings

commit 93448500d56a4652a3a060b274936c40015ac8ec
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Dec 7 10:55:25 2023 +0200

    home: imp code

commit eb32f8268bee097a81463ba29f7ea52be6e7d88b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Dec 7 10:42:23 2023 +0200

    home: imp code

commit 873d1412cf7c07ed985985a47325779bcfbf650a
Merge: 627659680 214175eb4
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Dec 7 10:22:25 2023 +0200

    Merge remote-tracking branch 'origin/master' into AG-28194-plain-dns

commit 627659680da8e973a3878d1722b276d30c7a27bb
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 6 17:39:14 2023 +0300

    client: handle plain dns setting

commit ffdbf05fede721d271a84482a5759284d18eb189
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Dec 1 15:12:50 2023 +0200

    home: http dns plain

... and 1 more commit
---
 CHANGELOG.md                                  |   5 +
 client/src/__locales/en.json                  |   3 +
 .../components/Settings/Encryption/Form.js    |  61 +++++---
 .../components/Settings/Encryption/index.js   |   5 +-
 client/src/helpers/form.js                    |   2 +-
 client/src/helpers/validators.js              |  15 ++
 client/src/reducers/encryption.js             |   1 +
 internal/home/home.go                         |   2 +-
 internal/home/tls.go                          | 132 ++++++++++--------
 openapi/CHANGELOG.md                          |   6 +
 openapi/openapi.yaml                          |   5 +
 11 files changed, 156 insertions(+), 81 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 889bac88..780265e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,11 @@ See also the [v0.107.44 GitHub milestone][ms-v0.107.44].
 NOTE: Add new changes BELOW THIS COMMENT.
 -->
 
+### Added
+
+- Ability to disable plain-DNS serving via UI if an encrypted protocol is
+  already used ([#1660]).
+
 <!--
 NOTE: Add new changes ABOVE THIS COMMENT.
 -->
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index a5839c0f..112928be 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -423,6 +423,9 @@
     "encryption_hostnames": "Hostnames",
     "encryption_reset": "Are you sure you want to reset encryption settings?",
     "encryption_warning": "Warning",
+    "encryption_plain_dns_enable": "Enable plain DNS",
+    "encryption_plain_dns_desc": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must enable at least one encrypted DNS protocol",
+    "encryption_plain_dns_error": "To disable plain DNS, enable at least one encrypted DNS protocol",
     "topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
     "topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
     "form_error_port_range": "Enter port number in the range of 80-65535",
diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js
index de7a7158..393b072d 100644
--- a/client/src/components/Settings/Encryption/Form.js
+++ b/client/src/components/Settings/Encryption/Form.js
@@ -12,7 +12,7 @@ import {
     toNumber,
 } from '../../../helpers/form';
 import {
-    validateServerName, validateIsSafePort, validatePort, validatePortQuic, validatePortTLS,
+    validateServerName, validateIsSafePort, validatePort, validatePortQuic, validatePortTLS, validatePlainDns,
 } from '../../../helpers/validators';
 import i18n from '../../../i18n';
 import KeyStatus from './KeyStatus';
@@ -47,6 +47,7 @@ const clearFields = (change, setTlsConfig, validateTlsConfig, t) => {
         force_https: false,
         enabled: false,
         private_key_saved: false,
+        serve_plain_dns: true,
     };
     // eslint-disable-next-line no-alert
     if (window.confirm(t('encryption_reset'))) {
@@ -83,6 +84,7 @@ let Form = (props) => {
         handleSubmit,
         handleChange,
         isEnabled,
+        servePlainDns,
         certificateChain,
         privateKey,
         certificatePath,
@@ -109,21 +111,24 @@ let Form = (props) => {
         privateKeySaved,
     } = props;
 
-    const isSavingDisabled = invalid
-        || submitting
-        || processingConfig
-        || processingValidate
-        || !valid_key
-        || !valid_cert
-        || !valid_pair;
+    const isSavingDisabled = () => {
+        const processing = submitting || processingConfig || processingValidate;
 
+        if (servePlainDns && !isEnabled) {
+            return invalid || processing;
+        }
+
+        return invalid || processing || !valid_key || !valid_cert || !valid_pair;
+    };
+
+    const isDisabled = isSavingDisabled();
     const isWarning = valid_key && valid_cert && valid_pair;
 
     return (
         <form onSubmit={handleSubmit}>
             <div className="row">
                 <div className="col-12">
-                    <div className="form__group form__group--settings">
+                    <div className="form__group form__group--settings mb-3">
                         <Field
                             name="enabled"
                             type="checkbox"
@@ -135,6 +140,19 @@ let Form = (props) => {
                     <div className="form__desc">
                         <Trans>encryption_enable_desc</Trans>
                     </div>
+                    <div className="form__group mb-3 mt-5">
+                        <Field
+                            name="serve_plain_dns"
+                            type="checkbox"
+                            component={CheckboxField}
+                            placeholder={t('encryption_plain_dns_enable')}
+                            onChange={handleChange}
+                            validate={validatePlainDns}
+                        />
+                    </div>
+                    <div className="form__desc">
+                        <Trans>encryption_plain_dns_desc</Trans>
+                    </div>
                     <hr />
                 </div>
                 <div className="col-12">
@@ -227,16 +245,16 @@ let Form = (props) => {
                             <Trans>encryption_doq</Trans>
                         </label>
                         <Field
-                                id="port_dns_over_quic"
-                                name="port_dns_over_quic"
-                                component={renderInputField}
-                                type="number"
-                                className="form-control"
-                                placeholder={t('encryption_doq')}
-                                validate={[validatePortQuic]}
-                                normalize={toNumber}
-                                onChange={handleChange}
-                                disabled={!isEnabled}
+                            id="port_dns_over_quic"
+                            name="port_dns_over_quic"
+                            component={renderInputField}
+                            type="number"
+                            className="form-control"
+                            placeholder={t('encryption_doq')}
+                            validate={[validatePortQuic]}
+                            normalize={toNumber}
+                            onChange={handleChange}
+                            disabled={!isEnabled}
                         />
                         <div className="form__desc">
                             <Trans>encryption_doq_desc</Trans>
@@ -412,8 +430,8 @@ let Form = (props) => {
             <div className="btn-list mt-2">
                 <button
                     type="submit"
+                    disabled={isDisabled}
                     className="btn btn-success btn-standart"
-                    disabled={isSavingDisabled}
                 >
                     <Trans>save_config</Trans>
                 </button>
@@ -434,6 +452,7 @@ Form.propTypes = {
     handleSubmit: PropTypes.func.isRequired,
     handleChange: PropTypes.func,
     isEnabled: PropTypes.bool.isRequired,
+    servePlainDns: PropTypes.bool.isRequired,
     certificateChain: PropTypes.string.isRequired,
     privateKey: PropTypes.string.isRequired,
     certificatePath: PropTypes.string.isRequired,
@@ -467,6 +486,7 @@ const selector = formValueSelector(FORM_NAME.ENCRYPTION);
 
 Form = connect((state) => {
     const isEnabled = selector(state, 'enabled');
+    const servePlainDns = selector(state, 'serve_plain_dns');
     const certificateChain = selector(state, 'certificate_chain');
     const privateKey = selector(state, 'private_key');
     const certificatePath = selector(state, 'certificate_path');
@@ -476,6 +496,7 @@ Form = connect((state) => {
     const privateKeySaved = selector(state, 'private_key_saved');
     return {
         isEnabled,
+        servePlainDns,
         certificateChain,
         privateKey,
         certificatePath,
diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js
index 4e4cef67..bcf610c3 100644
--- a/client/src/components/Settings/Encryption/index.js
+++ b/client/src/components/Settings/Encryption/index.js
@@ -25,7 +25,8 @@ class Encryption extends Component {
 
     handleFormChange = debounce((values) => {
         const submitValues = this.getSubmitValues(values);
-        if (submitValues.enabled) {
+
+        if (submitValues.enabled || submitValues.serve_plain_dns) {
             this.props.validateTlsConfig(submitValues);
         }
     }, DEBOUNCE_TIMEOUT);
@@ -85,6 +86,7 @@ class Encryption extends Component {
             certificate_path,
             private_key_path,
             private_key_saved,
+            serve_plain_dns,
         } = encryption;
 
         const initialValues = this.getInitialValues({
@@ -99,6 +101,7 @@ class Encryption extends Component {
             certificate_path,
             private_key_path,
             private_key_saved,
+            serve_plain_dns,
         });
 
         return (
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index f58aa830..1b7b4997 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -180,7 +180,7 @@ export const CheckboxField = ({
     {!disabled
     && touched
     && error
-    && <span className="form__message form__message--error"><Trans>{error}</Trans></span>}
+    && <div className="form__message form__message--error mt-1"><Trans>{error}</Trans></div>}
 </>;
 
 CheckboxField.propTypes = {
diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js
index e9d100a8..76e79d6f 100644
--- a/client/src/helpers/validators.js
+++ b/client/src/helpers/validators.js
@@ -389,3 +389,18 @@ export const validateIPv6Subnet = (value) => {
     }
     return undefined;
 };
+
+/**
+ * @returns {undefined|string}
+ * @param value
+ * @param allValues
+ */
+export const validatePlainDns = (value, allValues) => {
+    const { enabled } = allValues;
+
+    if (!enabled && !value) {
+        return 'encryption_plain_dns_error';
+    }
+
+    return undefined;
+};
diff --git a/client/src/reducers/encryption.js b/client/src/reducers/encryption.js
index 8fe9a2cb..6b04a49a 100644
--- a/client/src/reducers/encryption.js
+++ b/client/src/reducers/encryption.js
@@ -62,6 +62,7 @@ const encryption = handleActions({
     processingConfig: false,
     processingValidate: false,
     enabled: false,
+    serve_plain_dns: false,
     dns_names: null,
     force_https: false,
     issuer: '',
diff --git a/internal/home/home.go b/internal/home/home.go
index 18d6a961..f0a037c0 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -608,7 +608,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
 	Context.auth, err = initUsers()
 	fatalOnError(err)
 
-	Context.tls, err = newTLSManager(config.TLS)
+	Context.tls, err = newTLSManager(config.TLS, config.DNS.ServePlainDNS)
 	if err != nil {
 		log.Error("initializing tls: %s", err)
 		onConfigModified()
diff --git a/internal/home/tls.go b/internal/home/tls.go
index 004e9412..e022d043 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -38,15 +38,19 @@ type tlsManager struct {
 
 	confLock sync.Mutex
 	conf     tlsConfigSettings
+
+	// servePlainDNS defines if plain DNS is allowed for incoming requests.
+	servePlainDNS bool
 }
 
 // newTLSManager initializes the manager of TLS configuration.  m is always
 // non-nil while any returned error indicates that the TLS configuration isn't
 // valid.  Thus TLS may be initialized later, e.g. via the web UI.
-func newTLSManager(conf tlsConfigSettings) (m *tlsManager, err error) {
+func newTLSManager(conf tlsConfigSettings, servePlainDNS bool) (m *tlsManager, err error) {
 	m = &tlsManager{
-		status: &tlsConfigStatus{},
-		conf:   conf,
+		status:        &tlsConfigStatus{},
+		conf:          conf,
+		servePlainDNS: servePlainDNS,
 	}
 
 	if m.conf.Enabled {
@@ -283,21 +287,29 @@ type tlsConfig struct {
 	tlsConfigSettingsExt `json:",inline"`
 }
 
-// tlsConfigSettingsExt is used to (un)marshal the PrivateKeySaved field to
-// ensure that clients don't send and receive previously saved private keys.
+// tlsConfigSettingsExt is used to (un)marshal PrivateKeySaved field and
+// ServePlainDNS field.
 type tlsConfigSettingsExt struct {
 	tlsConfigSettings `json:",inline"`
 
 	// PrivateKeySaved is true if the private key is saved as a string and omit
-	// key from answer.
-	PrivateKeySaved bool `yaml:"-" json:"private_key_saved,inline"`
+	// key from answer.  It is used to ensure that clients don't send and
+	// receive previously saved private keys.
+	PrivateKeySaved bool `yaml:"-" json:"private_key_saved"`
+
+	// ServePlainDNS defines if plain DNS is allowed for incoming requests.  It
+	// is an [aghalg.NullBool] to be able to tell when it's set without using
+	// pointers.
+	ServePlainDNS aghalg.NullBool `yaml:"-" json:"serve_plain_dns"`
 }
 
+// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
 func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
 	m.confLock.Lock()
 	data := tlsConfig{
 		tlsConfigSettingsExt: tlsConfigSettingsExt{
 			tlsConfigSettings: m.conf,
+			ServePlainDNS:     aghalg.BoolToNullBool(m.servePlainDNS),
 		},
 		tlsConfigStatus: m.status,
 	}
@@ -306,6 +318,7 @@ func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
 	marshalTLS(w, r, data)
 }
 
+// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.
 func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
 	setts, err := unmarshalTLS(r)
 	if err != nil {
@@ -318,30 +331,8 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
 		setts.PrivateKey = m.conf.PrivateKey
 	}
 
-	if setts.Enabled {
-		err = validatePorts(
-			tcpPort(config.HTTPConfig.Address.Port()),
-			tcpPort(setts.PortHTTPS),
-			tcpPort(setts.PortDNSOverTLS),
-			tcpPort(setts.PortDNSCrypt),
-			udpPort(config.DNS.Port),
-			udpPort(setts.PortDNSOverQUIC),
-		)
-		if err != nil {
-			aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
-
-			return
-		}
-	}
-
-	if !webCheckPortAvailable(setts.PortHTTPS) {
-		aghhttp.Error(
-			r,
-			w,
-			http.StatusBadRequest,
-			"port %d is not available, cannot enable HTTPS on it",
-			setts.PortHTTPS,
-		)
+	if err = validateTLSSettings(setts); err != nil {
+		aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
 
 		return
 	}
@@ -358,7 +349,12 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
 	marshalTLS(w, r, resp)
 }
 
-func (m *tlsManager) setConfig(newConf tlsConfigSettings, status *tlsConfigStatus) (restartHTTPS bool) {
+// setConfig updates manager conf with the given one.
+func (m *tlsManager) setConfig(
+	newConf tlsConfigSettings,
+	status *tlsConfigStatus,
+	servePlain aghalg.NullBool,
+) (restartHTTPS bool) {
 	m.confLock.Lock()
 	defer m.confLock.Unlock()
 
@@ -390,9 +386,15 @@ func (m *tlsManager) setConfig(newConf tlsConfigSettings, status *tlsConfigStatu
 	m.conf.PrivateKeyData = newConf.PrivateKeyData
 	m.status = status
 
+	if servePlain != aghalg.NBNull {
+		m.servePlainDNS = servePlain == aghalg.NBTrue
+	}
+
 	return restartHTTPS
 }
 
+// handleTLSConfigure is the handler for the POST /control/tls/configure HTTP
+// API.
 func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
 	req, err := unmarshalTLS(r)
 	if err != nil {
@@ -405,31 +407,8 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
 		req.PrivateKey = m.conf.PrivateKey
 	}
 
-	if req.Enabled {
-		err = validatePorts(
-			tcpPort(config.HTTPConfig.Address.Port()),
-			tcpPort(req.PortHTTPS),
-			tcpPort(req.PortDNSOverTLS),
-			tcpPort(req.PortDNSCrypt),
-			udpPort(config.DNS.Port),
-			udpPort(req.PortDNSOverQUIC),
-		)
-		if err != nil {
-			aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
-
-			return
-		}
-	}
-
-	// TODO(e.burkov):  Investigate and perhaps check other ports.
-	if !webCheckPortAvailable(req.PortHTTPS) {
-		aghhttp.Error(
-			r,
-			w,
-			http.StatusBadRequest,
-			"port %d is not available, cannot enable https on it",
-			req.PortHTTPS,
-		)
+	if err = validateTLSSettings(req); err != nil {
+		aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
 
 		return
 	}
@@ -447,8 +426,18 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	restartHTTPS := m.setConfig(req.tlsConfigSettings, status)
+	restartHTTPS := m.setConfig(req.tlsConfigSettings, status, req.ServePlainDNS)
 	m.setCertFileTime()
+
+	if req.ServePlainDNS != aghalg.NBNull {
+		func() {
+			m.confLock.Lock()
+			defer m.confLock.Unlock()
+
+			config.DNS.ServePlainDNS = req.ServePlainDNS == aghalg.NBTrue
+		}()
+	}
+
 	onConfigModified()
 
 	err = reconfigureDNSServer()
@@ -479,6 +468,33 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
 	}
 }
 
+// validateTLSSettings returns error if the setts are not valid.
+func validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
+	if setts.Enabled {
+		err = validatePorts(
+			tcpPort(config.HTTPConfig.Address.Port()),
+			tcpPort(setts.PortHTTPS),
+			tcpPort(setts.PortDNSOverTLS),
+			tcpPort(setts.PortDNSCrypt),
+			udpPort(config.DNS.Port),
+			udpPort(setts.PortDNSOverQUIC),
+		)
+		if err != nil {
+			// Don't wrap the error since it's informative enough as is.
+			return err
+		}
+	} else if setts.ServePlainDNS == aghalg.NBFalse {
+		// TODO(a.garipov): Support full disabling of all DNS.
+		return errors.Error("plain DNS is required in case encryption protocols are disabled")
+	}
+
+	if !webCheckPortAvailable(setts.PortHTTPS) {
+		return fmt.Errorf("port %d is not available, cannot enable HTTPS on it", setts.PortHTTPS)
+	}
+
+	return nil
+}
+
 // validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home
 // DNS protocols.
 func validatePorts(
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index 16e7ab0d..b71ee56c 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -6,6 +6,12 @@
 
 ## v0.107.42: API changes
 
+### The new field `"serve_plain_dns"` in `TlsConfig`
+
+* The new field `"serve_plain_dns"` in `POST /control/tls/configure`,
+  `POST /control/tls/validate` and `GET /control/tls/status` is true if plain
+  DNS is allowed for incoming requests.
+
 ### The new fields `"upstreams_cache_enabled"` and `"upstreams_cache_size"` in `Client` object
 
 * The new field `"upstreams_cache_enabled"` in `GET /control/clients`,
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index c105ee1d..5ab3fa52 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -2463,6 +2463,11 @@
           'example': true
           'description': >
             Set to true if both certificate and private key are correct.
+        'serve_plain_dns':
+          'type': 'boolean'
+          'example': true
+          'description': >
+            Set to true if plain DNS is allowed for incoming requests.
     'NetInterface':
       'type': 'object'
       'description': 'Network interface info'