From c908eec5de2a04ab7b38b92e0e4eac9958909b19 Mon Sep 17 00:00:00 2001 From: Dimitry Kolyshev 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 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 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 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 Date: Fri Dec 8 15:22:27 2023 +0200 Revert "safesearch: imp docs" This reverts commit bab6bf3467f8914a34413bbbcdc37e89ff0401a5. commit bab6bf3467f8914a34413bbbcdc37e89ff0401a5 Author: Dimitry Kolyshev Date: Fri Dec 8 15:21:23 2023 +0200 safesearch: imp docs commit aa5e6e30e01bf947d645ac4a9578eeac09c92a19 Merge: 503888447 2b62901fe Author: Dimitry Kolyshev 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 Date: Fri Dec 8 14:47:23 2023 +0200 home: imp code commit 2b62901feb29c9613ae648fa5e83598157207a17 Author: Ildar Kamalov Date: Fri Dec 8 11:55:25 2023 +0300 client: add plain dns description commit 3d51fc8ea1955e599953070a4b330dd4e2fd44bc Author: Dimitry Kolyshev Date: Fri Dec 8 10:15:53 2023 +0200 all: changelog commit 59697b5f1ab049bd2259ffe42cef7223531ef7aa Merge: 81a15d081 b668c04ea Author: Dimitry Kolyshev 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 Date: Thu Dec 7 17:30:05 2023 +0300 client/src/__locales/en.json edited online with Bitbucket commit 0cf2f880fbd1592c02e6df42319cba357f0d7bc8 Author: Natalia Sokolova Date: Thu Dec 7 17:29:51 2023 +0300 client/src/__locales/en.json edited online with Bitbucket commit 2f32c59b8b1d764d060a69c35787566cf5210063 Author: Dimitry Kolyshev Date: Thu Dec 7 13:14:04 2023 +0200 home: imp code commit 01e21a26bdd13c42c55c8ea3b5bbe84933bf0c04 Author: Dimitry Kolyshev Date: Thu Dec 7 12:14:02 2023 +0200 all: imp docs commit b6beec6df7c2a9077ddce018656c701b7e875b53 Author: Ildar Kamalov Date: Thu Dec 7 12:42:21 2023 +0300 client: fix reset settings commit 93448500d56a4652a3a060b274936c40015ac8ec Author: Dimitry Kolyshev Date: Thu Dec 7 10:55:25 2023 +0200 home: imp code commit eb32f8268bee097a81463ba29f7ea52be6e7d88b Author: Dimitry Kolyshev Date: Thu Dec 7 10:42:23 2023 +0200 home: imp code commit 873d1412cf7c07ed985985a47325779bcfbf650a Merge: 627659680 214175eb4 Author: Dimitry Kolyshev 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 Date: Wed Dec 6 17:39:14 2023 +0300 client: handle plain dns setting commit ffdbf05fede721d271a84482a5759284d18eb189 Author: Dimitry Kolyshev 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]). + 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.", "topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings.", "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 (
-
+
{
encryption_enable_desc
+
+ +
+
+ encryption_plain_dns_desc +

@@ -227,16 +245,16 @@ let Form = (props) => { encryption_doq
encryption_doq_desc @@ -412,8 +430,8 @@ let Form = (props) => {
@@ -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 - && {error}} + &&
{error}
} ; 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'