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
This commit is contained in:
Dimitry Kolyshev 2023-12-12 13:16:01 +03:00
parent 79d7a1ef46
commit c908eec5de
11 changed files with 156 additions and 81 deletions

View File

@ -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.
-->

View File

@ -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",

View File

@ -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,

View File

@ -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 (

View File

@ -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 = {

View File

@ -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;
};

View File

@ -62,6 +62,7 @@ const encryption = handleActions({
processingConfig: false,
processingValidate: false,
enabled: false,
serve_plain_dns: false,
dns_names: null,
force_https: false,
issuer: '',

View File

@ -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()

View File

@ -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(

View File

@ -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`,

View File

@ -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'