Pull request 2382: AGDNS-2714-tls-config

Merge in DNS/adguard-home from AGDNS-2714-tls-config to master

Squashed commit of the following:

commit 073e5ec367db02690e9527602a1da6bfd29321a0
Merge: 18f38c9d4 4d258972d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 16 18:25:23 2025 +0300

    Merge branch 'master' into AGDNS-2714-tls-config

commit 18f38c9d44337752c6d0f09142658f374de0979f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 11 15:02:00 2025 +0300

    dnsforward: imp docs

commit ed56d3c2bc239bdc9af000d847721c4c43d173a3
Merge: 3ef281ea2 1cc6c00e4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 17:25:08 2025 +0300

    Merge branch 'master' into AGDNS-2714-tls-config

commit 3ef281ea28dc1fcab0a1291fb3221e6324077a10
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 17:24:29 2025 +0300

    all: imp docs

commit b75f2874a816d4814d218c3b062d532f02e26ca5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 7 17:16:59 2025 +0300

    dnsforward: imp code

commit 8ab17b96bca957a172062faaa23b72d5c7ed4d0d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 4 21:26:37 2025 +0300

    all: imp code

commit 1abce97b50fe0406dd1ec85b96a0f99b633325cc
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 2 18:22:15 2025 +0300

    home: imp code

commit debf710f4ebbdfe3e4d2f15b1adcf6b86f8dfc0d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 1 14:52:21 2025 +0300

    home: imp code

commit 4aa26f15b721f2a3f32da29b3f664a02bc5a8608
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 1 14:16:16 2025 +0300

    all: imp code

commit 1a3e72f7a1276f9f797caf9b615f8a552cc9e988
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 31 21:22:40 2025 +0300

    all: imp code

commit 776ab824aef18ea27b59c02ebfc8620c715a867e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 27 14:00:33 2025 +0300

    home: tls config mu

commit 9ebf912f530181043df5c583e82291484996429a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 26 18:58:47 2025 +0300

    all: tls config
This commit is contained in:
Stanislav Chzhen
2025-04-16 18:57:04 +03:00
parent 4d258972d1
commit 3521e8ed9f
21 changed files with 412 additions and 293 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/netip"
"os"
"path/filepath"
"slices"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
@@ -23,6 +24,7 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/google/go-cmp/cmp"
"github.com/google/renameio/v2/maybe"
yaml "gopkg.in/yaml.v3"
)
@@ -263,28 +265,116 @@ type dnsConfig struct {
HostsFileEnabled bool `yaml:"hostsfile_enabled"`
}
// tlsConfigSettings is the TLS configuration for DNS-over-TLS, DNS-over-QUIC,
// and HTTPS. When adding new properties, update the [tlsConfigSettings.clone]
// and [tlsConfigSettings.setPrivateFieldsAndCompare] methods as necessary.
type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DoT/DoH/HTTPS) status
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
ForceHTTPS bool `yaml:"force_https" json:"force_https"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
PortHTTPS uint16 `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
PortDNSOverTLS uint16 `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DoT will be disabled
PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"` // DNS-over-QUIC port. If 0, DoQ will be disabled
// Enabled indicates whether encryption (DoT/DoH/HTTPS) is enabled.
Enabled bool `yaml:"enabled" json:"enabled"`
// PortDNSCrypt is the port for DNSCrypt requests. If it's zero,
// DNSCrypt is disabled.
// ServerName is the hostname of the HTTPS/TLS server.
ServerName string `yaml:"server_name" json:"server_name,omitempty"`
// ForceHTTPS, if true, forces an HTTP to HTTPS redirect.
ForceHTTPS bool `yaml:"force_https" json:"force_https"`
// PortHTTPS is the HTTPS port. If 0, HTTPS will be disabled.
PortHTTPS uint16 `yaml:"port_https" json:"port_https,omitempty"`
// PortDNSOverTLS is the DNS-over-TLS port. If 0, DoT will be disabled.
PortDNSOverTLS uint16 `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"`
// PortDNSOverQUIC is the DNS-over-QUIC port. If 0, DoQ will be disabled.
PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"`
// PortDNSCrypt is the port for DNSCrypt requests. If it's zero, DNSCrypt
// is disabled.
PortDNSCrypt uint16 `yaml:"port_dnscrypt" json:"port_dnscrypt"`
// DNSCryptConfigFile is the path to the DNSCrypt config file. Must be
// set if PortDNSCrypt is not zero.
// DNSCryptConfigFile is the path to the DNSCrypt config file. Must be set
// if PortDNSCrypt is not zero.
//
// See https://github.com/AdguardTeam/dnsproxy and
// https://github.com/ameshkov/dnscrypt.
DNSCryptConfigFile string `yaml:"dnscrypt_config_file" json:"dnscrypt_config_file"`
// Allow DoH queries via unencrypted HTTP (e.g. for reverse proxying)
// AllowUnencryptedDoH allows DoH queries via unencrypted HTTP (e.g. for
// reverse proxying).
//
// TODO(s.chzhen): Add this option into the Web UI.
AllowUnencryptedDoH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
// CertificateChain is the PEM-encoded certificate chain. Must be empty if
// [tlsConfigSettings.CertificatePath] is provided.
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"`
// PrivateKey is the PEM-encoded private key. Must be empty if
// [tlsConfigSettings.PrivateKeyPath] is provided.
PrivateKey string `yaml:"private_key" json:"private_key"`
// CertificatePath is the path to the certificate file. Must be empty if
// [tlsConfigSettings.CertificateChain] is provided.
CertificatePath string `yaml:"certificate_path" json:"certificate_path"`
// PrivateKeyPath is the path to the private key file. Must be empty if
// [tlsConfigSettings.PrivateKey] is provided.
PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"`
// OverrideTLSCiphers, when set, contains the names of the cipher suites to
// use. If the slice is empty, the default safe suites are used.
OverrideTLSCiphers []string `yaml:"override_tls_ciphers,omitempty" json:"-"`
// CertificateChainData is the PEM-encoded byte data for the certificate
// chain.
CertificateChainData []byte `yaml:"-" json:"-"`
// PrivateKeyData is the PEM-encoded byte data for the private key.
PrivateKeyData []byte `yaml:"-" json:"-"`
// StrictSNICheck controls if the connections with SNI mismatching the
// certificate's ones should be rejected.
StrictSNICheck bool `yaml:"strict_sni_check" json:"-"`
}
// clone returns a deep copy of c.
func (c *tlsConfigSettings) clone() (clone *tlsConfigSettings) {
clone = &tlsConfigSettings{}
*clone = *c
clone.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)
clone.CertificateChainData = slices.Clone(c.CertificateChainData)
clone.PrivateKeyData = slices.Clone(c.PrivateKeyData)
return clone
}
// setPrivateFieldsAndCompare sets any missing properties in conf to match those
// in c and returns true if TLS configurations are equal. conf must not be be
// nil.
// It sets the following properties because these are not accepted from the
// frontend:
//
// [tlsConfigSettings.AllowUnencryptedDoH]
// [tlsConfigSettings.DNSCryptConfigFile]
// [tlsConfigSettings.OverrideTLSCiphers]
// [tlsConfigSettings.PortDNSCrypt]
//
// The following properties are skipped as they are set by
// [tlsManager.loadTLSConfig]:
//
// [tlsConfigSettings.CertificateChainData]
// [tlsConfigSettings.PrivateKeyData]
func (c *tlsConfigSettings) setPrivateFieldsAndCompare(conf *tlsConfigSettings) (equal bool) {
conf.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)
// TODO(s.chzhen): Remove this once the frontend supports it.
conf.AllowUnencryptedDoH = c.AllowUnencryptedDoH
conf.DNSCryptConfigFile = c.DNSCryptConfigFile
conf.PortDNSCrypt = c.PortDNSCrypt
// TODO(a.garipov): Define a custom comparer.
return cmp.Equal(c, conf)
}
type queryLogConfig struct {
@@ -649,9 +739,8 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
}
if tlsMgr != nil {
tlsConf := tlsConfigSettings{}
tlsMgr.WriteDiskConfig(&tlsConf)
config.TLS = tlsConf
tlsConf := tlsMgr.config()
config.TLS = *tlsConf
}
if globalContext.stats != nil {

View File

@@ -164,11 +164,8 @@ func (vr *versionResponse) setAllowedToAutoUpdate(tlsMgr *tlsManager) (err error
return nil
}
tlsConf := &tlsConfigSettings{}
tlsMgr.WriteDiskConfig(tlsConf)
canUpdate := true
if tlsConfUsesPrivilegedPorts(tlsConf) ||
if tlsConfUsesPrivilegedPorts(tlsMgr.config()) ||
config.HTTPConfig.Address.Port() < 1024 ||
config.DNS.Port < 1024 {
canUpdate, err = aghnet.CanBindPrivilegedPorts()

View File

@@ -2,6 +2,7 @@ package home
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net"
@@ -111,9 +112,6 @@ func initDNS(
return err
}
tlsConf := &tlsConfigSettings{}
tlsMgr.WriteDiskConfig(tlsConf)
return initDNSServer(
globalContext.filters,
globalContext.stats,
@@ -121,7 +119,7 @@ func initDNS(
globalContext.dhcpServer,
anonymizer,
httpRegister,
tlsConf,
tlsMgr.config(),
tlsMgr,
baseLogger,
)
@@ -255,11 +253,16 @@ func newServerConfig(
fwdConf := dnsConf.Config
fwdConf.ClientsContainer = clientsContainer
intTLSConf, err := newDNSTLSConfig(tlsConf, hosts)
if err != nil {
return nil, fmt.Errorf("constructing tls config: %w", err)
}
newConf = &dnsforward.ServerConfig{
UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port),
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
Config: fwdConf,
TLSConfig: newDNSTLSConfig(tlsConf, hosts),
TLSConf: intTLSConf,
TLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH,
UpstreamTimeout: time.Duration(dnsConf.UpstreamTimeout),
TLSv12Roots: tlsMgr.rootCerts,
@@ -304,14 +307,25 @@ func newServerConfig(
}
// newDNSTLSConfig converts values from the configuration file into the internal
// TLS settings for the DNS server. tlsConf must not be nil.
func newDNSTLSConfig(conf *tlsConfigSettings, addrs []netip.Addr) (dnsConf dnsforward.TLSConfig) {
// TLS settings for the DNS server. conf must not be nil.
func newDNSTLSConfig(
conf *tlsConfigSettings,
addrs []netip.Addr,
) (dnsConf *dnsforward.TLSConfig, err error) {
if !conf.Enabled {
return dnsforward.TLSConfig{}
return &dnsforward.TLSConfig{}, nil
}
dnsConf = conf.TLSConfig
dnsConf.ServerName = conf.ServerName
cert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)
if err != nil {
return nil, fmt.Errorf("parsing tls key pair: %w", err)
}
dnsConf = &dnsforward.TLSConfig{
Cert: &cert,
ServerName: conf.ServerName,
StrictSNICheck: conf.StrictSNICheck,
}
if conf.PortHTTPS != 0 {
dnsConf.HTTPSListenAddrs = ipsToTCPAddrs(addrs, conf.PortHTTPS)
@@ -325,7 +339,7 @@ func newDNSTLSConfig(conf *tlsConfigSettings, addrs []netip.Addr) (dnsConf dnsfo
dnsConf.QUICListenAddrs = ipsToUDPAddrs(addrs, conf.PortDNSOverQUIC)
}
return dnsConf
return dnsConf, nil
}
// newDNSCryptConfig converts values from the configuration file into the
@@ -378,8 +392,7 @@ type dnsEncryption struct {
// getDNSEncryption returns the TLS encryption addresses that AdGuard Home
// listens on. tlsMgr must not be nil.
func getDNSEncryption(tlsMgr *tlsManager) (de dnsEncryption) {
tlsConf := tlsConfigSettings{}
tlsMgr.WriteDiskConfig(&tlsConf)
tlsConf := tlsMgr.config()
if !tlsConf.Enabled || len(tlsConf.ServerName) == 0 {
return dnsEncryption{}

View File

@@ -991,9 +991,9 @@ func printWebAddrs(proto, addr string, port uint16) {
//
// TODO(s.chzhen): Implement separate functions for HTTP and HTTPS.
func printHTTPAddresses(proto string, tlsMgr *tlsManager) {
tlsConf := tlsConfigSettings{}
var tlsConf *tlsConfigSettings
if tlsMgr != nil {
tlsMgr.WriteDiskConfig(&tlsConf)
tlsConf = tlsMgr.config()
}
port := config.HTTPConfig.Address.Port()

View File

@@ -24,11 +24,9 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/c2h5oh/datasize"
"github.com/google/go-cmp/cmp"
)
// tlsManager contains the current configuration and state of AdGuard Home TLS
@@ -37,6 +35,9 @@ type tlsManager struct {
// logger is used for logging the operation of the TLS Manager.
logger *slog.Logger
// mu protects status, certLastMod, conf, and servePlainDNS.
mu *sync.Mutex
// status is the current status of the configuration. It is never nil.
status *tlsConfigStatus
@@ -52,6 +53,9 @@ type tlsManager struct {
// Resolve it.
web *webAPI
// conf contains the TLS configuration settings. It must not be nil.
conf *tlsConfigSettings
// configModified is called when the TLS configuration is changed via an
// HTTP request.
configModified func()
@@ -59,9 +63,6 @@ type tlsManager struct {
// customCipherIDs are the ID of the cipher suites that AdGuard Home must use.
customCipherIDs []uint16
confLock sync.Mutex
conf tlsConfigSettings
// servePlainDNS defines if plain DNS is allowed for incoming requests.
servePlainDNS bool
}
@@ -91,9 +92,10 @@ type tlsManagerConfig struct {
func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {
m = &tlsManager{
logger: conf.logger,
mu: &sync.Mutex{},
configModified: conf.configModified,
status: &tlsConfigStatus{},
conf: conf.tlsSettings,
conf: &conf.tlsSettings,
servePlainDNS: conf.servePlainDNS,
}
@@ -112,17 +114,22 @@ func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager,
m.logger.InfoContext(ctx, "using default ciphers")
}
if m.conf.Enabled {
err = m.load(ctx)
if err != nil {
m.conf.Enabled = false
m.mu.Lock()
defer m.mu.Unlock()
return m, err
}
m.setCertFileTime(ctx)
if !m.conf.Enabled {
return m, nil
}
err = m.load(ctx)
if err != nil {
m.conf.Enabled = false
return m, err
}
m.setCertFileTime(ctx)
return m, nil
}
@@ -136,8 +143,9 @@ func (m *tlsManager) setWebAPI(webAPI *webAPI) {
}
// load reloads the TLS configuration from files or data from the config file.
// m.mu is expected to be locked.
func (m *tlsManager) load(ctx context.Context) (err error) {
err = m.loadTLSConf(ctx, &m.conf, m.status)
err = m.loadTLSConfig(ctx, m.conf, m.status)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
@@ -145,15 +153,16 @@ func (m *tlsManager) load(ctx context.Context) (err error) {
return nil
}
// WriteDiskConfig - write config
func (m *tlsManager) WriteDiskConfig(conf *tlsConfigSettings) {
m.confLock.Lock()
*conf = m.conf
m.confLock.Unlock()
// config returns a deep copy of the stored TLS configuration.
func (m *tlsManager) config() (conf *tlsConfigSettings) {
m.mu.Lock()
defer m.mu.Unlock()
return m.conf.clone()
}
// setCertFileTime sets [tlsManager.certLastMod] from the certificate. If there
// are errors, setCertFileTime logs them.
// are errors, setCertFileTime logs them. m.mu is expected to be locked.
func (m *tlsManager) setCertFileTime(ctx context.Context) {
if len(m.conf.CertificatePath) == 0 {
return
@@ -175,21 +184,21 @@ func (m *tlsManager) setCertFileTime(ctx context.Context) {
func (m *tlsManager) start(_ context.Context) {
m.registerWebHandlers()
m.confLock.Lock()
tlsConf := m.conf
m.confLock.Unlock()
m.mu.Lock()
defer m.mu.Unlock()
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
m.web.tlsConfigChanged(context.Background(), tlsConf)
m.web.tlsConfigChanged(context.Background(), m.conf)
}
// reload updates the configuration and restarts the TLS manager.
func (m *tlsManager) reload(ctx context.Context) {
m.confLock.Lock()
m.mu.Lock()
defer m.mu.Unlock()
tlsConf := m.conf
m.confLock.Unlock()
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
return
@@ -211,9 +220,7 @@ func (m *tlsManager) reload(ctx context.Context) {
m.logger.InfoContext(ctx, "certificate file is modified")
m.confLock.Lock()
err = m.load(ctx)
m.confLock.Unlock()
if err != nil {
m.logger.ErrorContext(ctx, "reloading", slogutil.KeyError, err)
@@ -227,10 +234,6 @@ func (m *tlsManager) reload(ctx context.Context) {
m.logger.ErrorContext(ctx, "reconfiguring dns server", slogutil.KeyError, err)
}
m.confLock.Lock()
tlsConf = m.conf
m.confLock.Unlock()
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
@@ -238,15 +241,12 @@ func (m *tlsManager) reload(ctx context.Context) {
}
// reconfigureDNSServer updates the DNS server configuration using the stored
// TLS settings.
// TLS settings. m.mu is expected to be locked.
func (m *tlsManager) reconfigureDNSServer() (err error) {
tlsConf := &tlsConfigSettings{}
m.WriteDiskConfig(tlsConf)
newConf, err := newServerConfig(
&config.DNS,
config.Clients.Sources,
tlsConf,
m.conf,
m,
httpRegister,
globalContext.clients.storage,
@@ -263,9 +263,11 @@ func (m *tlsManager) reconfigureDNSServer() (err error) {
return nil
}
// loadTLSConf loads and validates the TLS configuration. The returned error is
// also set in status.WarningValidation.
func (m *tlsManager) loadTLSConf(
// loadTLSConfig loads and validates the TLS configuration. It also sets
// [tlsConfigSettings.CertificateChainData] and
// [tlsConfigSettings.PrivateKeyData] properties. The returned error is also
// set in status.WarningValidation.
func (m *tlsManager) loadTLSConfig(
ctx context.Context,
tlsConf *tlsConfigSettings,
status *tlsConfigStatus,
@@ -357,10 +359,10 @@ type tlsConfigStatus struct {
KeyType string `json:"key_type,omitempty"`
// NotBefore is the NotBefore field of the first certificate in the chain.
NotBefore time.Time `json:"not_before,omitempty"`
NotBefore time.Time `json:"not_before"`
// NotAfter is the NotAfter field of the first certificate in the chain.
NotAfter time.Time `json:"not_after,omitempty"`
NotAfter time.Time `json:"not_after"`
// WarningValidation is a validation warning message with the issue
// description.
@@ -410,15 +412,23 @@ type tlsConfigSettingsExt struct {
// 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()
var tlsConf *tlsConfigSettings
var servePlainDNS bool
func() {
m.mu.Lock()
defer m.mu.Unlock()
tlsConf = m.conf.clone()
servePlainDNS = m.servePlainDNS
}()
data := tlsConfig{
tlsConfigSettingsExt: tlsConfigSettingsExt{
tlsConfigSettings: m.conf,
ServePlainDNS: aghalg.BoolToNullBool(m.servePlainDNS),
tlsConfigSettings: *tlsConf,
ServePlainDNS: aghalg.BoolToNullBool(servePlainDNS),
},
tlsConfigStatus: m.status,
}
m.confLock.Unlock()
marshalTLS(w, r, data)
}
@@ -434,6 +444,9 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
return
}
m.mu.Lock()
defer m.mu.Unlock()
if setts.PrivateKeySaved {
setts.PrivateKey = m.conf.PrivateKey
}
@@ -449,7 +462,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
// Skip the error check, since we are only interested in the value of
// status.WarningValidation.
status := &tlsConfigStatus{}
_ = m.loadTLSConf(ctx, &setts.tlsConfigSettings, status)
_ = m.loadTLSConfig(ctx, &setts.tlsConfigSettings, status)
resp := tlsConfig{
tlsConfigSettingsExt: setts,
tlsConfigStatus: status,
@@ -458,42 +471,23 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
marshalTLS(w, r, resp)
}
// setConfig updates manager conf with the given one.
// setConfig updates manager TLS configuration with the given one. m.mu is
// expected to be locked.
func (m *tlsManager) setConfig(
ctx context.Context,
newConf tlsConfigSettings,
status *tlsConfigStatus,
servePlain aghalg.NullBool,
) (restartHTTPS bool) {
m.confLock.Lock()
defer m.confLock.Unlock()
// Reset the DNSCrypt data before comparing, since we currently do not
// accept these from the frontend.
//
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
newConf.PortDNSCrypt = m.conf.PortDNSCrypt
if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
if !m.conf.setPrivateFieldsAndCompare(&newConf) {
m.logger.InfoContext(ctx, "config has changed, restarting https server")
restartHTTPS = true
} else {
m.logger.InfoContext(ctx, "config has not changed")
}
// Note: don't do just `t.conf = data` because we must preserve all other members of t.conf
m.conf.Enabled = newConf.Enabled
m.conf.ServerName = newConf.ServerName
m.conf.ForceHTTPS = newConf.ForceHTTPS
m.conf.PortHTTPS = newConf.PortHTTPS
m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
m.conf.CertificateChain = newConf.CertificateChain
m.conf.CertificatePath = newConf.CertificatePath
m.conf.CertificateChainData = newConf.CertificateChainData
m.conf.PrivateKey = newConf.PrivateKey
m.conf.PrivateKeyPath = newConf.PrivateKeyPath
m.conf.PrivateKeyData = newConf.PrivateKeyData
m.conf = &newConf
m.status = status
if servePlain != aghalg.NBNull {
@@ -515,6 +509,16 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
return
}
var restartHTTPS bool
defer func() {
if restartHTTPS {
m.configModified()
}
}()
m.mu.Lock()
defer m.mu.Unlock()
if req.PrivateKeySaved {
req.PrivateKey = m.conf.PrivateKey
}
@@ -526,7 +530,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
}
status := &tlsConfigStatus{}
err = m.loadTLSConf(ctx, &req.tlsConfigSettings, status)
err = m.loadTLSConfig(ctx, &req.tlsConfigSettings, status)
if err != nil {
resp := tlsConfig{
tlsConfigSettingsExt: req,
@@ -538,20 +542,18 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
return
}
restartHTTPS := m.setConfig(ctx, req.tlsConfigSettings, status, req.ServePlainDNS)
restartHTTPS = m.setConfig(ctx, req.tlsConfigSettings, status, req.ServePlainDNS)
m.setCertFileTime(ctx)
if req.ServePlainDNS != aghalg.NBNull {
func() {
m.confLock.Lock()
defer m.confLock.Unlock()
config.Lock()
defer config.Unlock()
config.DNS.ServePlainDNS = req.ServePlainDNS == aghalg.NBTrue
}()
}
m.configModified()
err = m.reconfigureDNSServer()
if err != nil {
m.logger.ErrorContext(ctx, "reconfiguring dns server", slogutil.KeyError, err)
@@ -567,18 +569,18 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
}
marshalTLS(w, r, resp)
if f, ok := w.(http.Flusher); ok {
f.Flush()
rc := http.NewResponseController(w)
err = rc.Flush()
if err != nil {
m.logger.ErrorContext(ctx, "flushing response", slogutil.KeyError, err)
}
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request. It is also should be done in a separate goroutine due to the
// request. It is also should be done in a separate goroutine due to the
// same reason.
if restartHTTPS {
go func() {
m.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
}()
go m.web.tlsConfigChanged(context.Background(), &req.tlsConfigSettings)
}
}

View File

@@ -239,11 +239,9 @@ func TestTLSManager_Reload(t *testing.T) {
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
Enabled: true,
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
servePlainDNS: false,
})
@@ -254,8 +252,7 @@ func TestTLSManager_Reload(t *testing.T) {
m.setWebAPI(web)
conf := &tlsConfigSettings{}
m.WriteDiskConfig(conf)
conf := m.config()
assertCertSerialNumber(t, conf, snBefore)
certDER, key = newCertAndKey(t, snAfter)
@@ -263,7 +260,7 @@ func TestTLSManager_Reload(t *testing.T) {
m.reload(ctx)
m.WriteDiskConfig(conf)
conf = m.config()
assertCertSerialNumber(t, conf, snAfter)
}
@@ -278,11 +275,9 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
Enabled: true,
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
servePlainDNS: false,
})
@@ -342,47 +337,49 @@ func TestValidateTLSSettings(t *testing.T) {
busyUDPPort := udpAddr.Port
testCases := []struct {
setts tlsConfigSettingsExt
name string
wantErr string
setts tlsConfigSettingsExt
}{{
name: "basic",
setts: tlsConfigSettingsExt{},
wantErr: "",
setts: tlsConfigSettingsExt{},
}, {
name: "disabled_all",
wantErr: "plain DNS is required in case encryption protocols are disabled",
setts: tlsConfigSettingsExt{
ServePlainDNS: aghalg.NBFalse,
},
name: "disabled_all",
wantErr: "plain DNS is required in case encryption protocols are disabled",
}, {
name: "busy_https_port",
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortHTTPS: uint16(busyTCPPort),
},
},
name: "busy_https_port",
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
}, {
name: "busy_dot_port",
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort),
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverTLS: uint16(busyTCPPort),
},
},
name: "busy_dot_port",
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort),
}, {
name: "busy_doq_port",
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverQUIC: uint16(busyUDPPort),
},
},
name: "busy_doq_port",
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
}, {
name: "duplicate_port",
wantErr: "validating tcp ports: duplicated values: [4433]",
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
@@ -390,8 +387,6 @@ func TestValidateTLSSettings(t *testing.T) {
PortDNSOverTLS: 4433,
},
},
name: "duplicate_port",
wantErr: "validating tcp ports: duplicated values: [4433]",
}}
for _, tc := range testCases {
@@ -417,11 +412,9 @@ func TestTLSManager_HandleTLSValidate(t *testing.T) {
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
Enabled: true,
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
servePlainDNS: false,
})
@@ -434,11 +427,9 @@ func TestTLSManager_HandleTLSValidate(t *testing.T) {
setts := &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
},
Enabled: true,
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
},
}
@@ -476,6 +467,7 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
require.NoError(t, err)
err = globalContext.dnsServer.Prepare(&dnsforward.ServerConfig{
TLSConf: &dnsforward.TLSConfig{},
Config: dnsforward.Config{
UpstreamMode: dnsforward.UpstreamModeLoadBalance,
EDNSClientSubnet: &dnsforward.EDNSClientSubnet{Enabled: false},
@@ -511,11 +503,9 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
Enabled: true,
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
servePlainDNS: true,
})
@@ -526,19 +516,16 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
m.setWebAPI(web)
conf := &tlsConfigSettings{}
m.WriteDiskConfig(conf)
conf := m.config()
assertCertSerialNumber(t, conf, wantSerialNumber)
// Prepare a request with the new TLS configuration.
setts := &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortHTTPS: 4433,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
},
Enabled: true,
PortHTTPS: 4433,
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
},
}

View File

@@ -157,8 +157,8 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
}
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary.
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
// if necessary. tlsConf must not be nil.
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *tlsConfigSettings) {
defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)
web.logger.DebugContext(ctx, "applying new tls configuration")