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'