all: sync with master
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"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"
|
||||
@@ -41,6 +43,22 @@ type tlsManager struct {
|
||||
// certLastMod is the last modification time of the certificate file.
|
||||
certLastMod time.Time
|
||||
|
||||
// rootCerts is a pool of root CAs for TLSv1.2.
|
||||
rootCerts *x509.CertPool
|
||||
|
||||
// web is the web UI and API server. It must not be nil.
|
||||
//
|
||||
// TODO(s.chzhen): Temporary cyclic dependency due to ongoing refactoring.
|
||||
// Resolve it.
|
||||
web *webAPI
|
||||
|
||||
// configModified is called when the TLS configuration is changed via an
|
||||
// HTTP request.
|
||||
configModified func()
|
||||
|
||||
// customCipherIDs are the ID of the cipher suites that AdGuard Home must use.
|
||||
customCipherIDs []uint16
|
||||
|
||||
confLock sync.Mutex
|
||||
conf tlsConfigSettings
|
||||
|
||||
@@ -48,21 +66,50 @@ type tlsManager struct {
|
||||
servePlainDNS bool
|
||||
}
|
||||
|
||||
// tlsManagerConfig contains the settings for initializing the TLS manager.
|
||||
type tlsManagerConfig struct {
|
||||
// logger is used for logging the operation of the TLS Manager. It must not
|
||||
// be nil.
|
||||
logger *slog.Logger
|
||||
|
||||
// configModified is called when the TLS configuration is changed via an
|
||||
// HTTP request. It must not be nil.
|
||||
configModified func()
|
||||
|
||||
// tlsSettings contains the TLS configuration settings.
|
||||
tlsSettings 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. logger must
|
||||
// not be nil.
|
||||
func newTLSManager(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
conf tlsConfigSettings,
|
||||
servePlainDNS bool,
|
||||
) (m *tlsManager, err error) {
|
||||
// valid. Thus TLS may be initialized later, e.g. via the web UI. conf must
|
||||
// not be nil. Note that [tlsManager.web] must be initialized later on by using
|
||||
// [tlsManager.setWebAPI].
|
||||
func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {
|
||||
m = &tlsManager{
|
||||
logger: logger,
|
||||
status: &tlsConfigStatus{},
|
||||
conf: conf,
|
||||
servePlainDNS: servePlainDNS,
|
||||
logger: conf.logger,
|
||||
configModified: conf.configModified,
|
||||
status: &tlsConfigStatus{},
|
||||
conf: conf.tlsSettings,
|
||||
servePlainDNS: conf.servePlainDNS,
|
||||
}
|
||||
|
||||
m.rootCerts = aghtls.SystemRootCAs()
|
||||
|
||||
if len(conf.tlsSettings.OverrideTLSCiphers) > 0 {
|
||||
m.customCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
|
||||
if err != nil {
|
||||
// Should not happen because upstreams are already validated. See
|
||||
// [validateTLSCipherIDs].
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.logger.InfoContext(ctx, "overriding ciphers", "ciphers", config.TLS.OverrideTLSCiphers)
|
||||
} else {
|
||||
m.logger.InfoContext(ctx, "using default ciphers")
|
||||
}
|
||||
|
||||
if m.conf.Enabled {
|
||||
@@ -79,6 +126,15 @@ func newTLSManager(
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// setWebAPI stores the provided web API. It must be called before
|
||||
// [tlsManager.start], [tlsManager.reload], [tlsManager.handleTLSConfigure], or
|
||||
// [tlsManager.validateTLSSettings].
|
||||
//
|
||||
// TODO(s.chzhen): Remove it once cyclic dependency is resolved.
|
||||
func (m *tlsManager) setWebAPI(webAPI *webAPI) {
|
||||
m.web = webAPI
|
||||
}
|
||||
|
||||
// load reloads the TLS configuration from files or data from the config file.
|
||||
func (m *tlsManager) load(ctx context.Context) (err error) {
|
||||
err = m.loadTLSConf(ctx, &m.conf, m.status)
|
||||
@@ -126,7 +182,7 @@ func (m *tlsManager) start(_ context.Context) {
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
m.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// reload updates the configuration and restarts the TLS manager.
|
||||
@@ -178,7 +234,7 @@ func (m *tlsManager) reload(ctx context.Context) {
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
m.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// reconfigureDNSServer updates the DNS server configuration using the stored
|
||||
@@ -191,6 +247,7 @@ func (m *tlsManager) reconfigureDNSServer() (err error) {
|
||||
&config.DNS,
|
||||
config.Clients.Sources,
|
||||
tlsConf,
|
||||
m,
|
||||
httpRegister,
|
||||
globalContext.clients.storage,
|
||||
)
|
||||
@@ -368,6 +425,8 @@ func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.
|
||||
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
setts, err := unmarshalTLS(r)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||
@@ -379,7 +438,9 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
setts.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if err = validateTLSSettings(setts); err != nil {
|
||||
if err = m.validateTLSSettings(setts); err != nil {
|
||||
m.logger.InfoContext(ctx, "validating tls settings", slogutil.KeyError, err)
|
||||
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
@@ -388,7 +449,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(r.Context(), &setts.tlsConfigSettings, status)
|
||||
_ = m.loadTLSConf(ctx, &setts.tlsConfigSettings, status)
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: setts,
|
||||
tlsConfigStatus: status,
|
||||
@@ -458,7 +519,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
req.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if err = validateTLSSettings(req); err != nil {
|
||||
if err = m.validateTLSSettings(req); err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
@@ -489,7 +550,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
}()
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
m.configModified()
|
||||
|
||||
err = m.reconfigureDNSServer()
|
||||
if err != nil {
|
||||
@@ -516,36 +577,54 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
// same reason.
|
||||
if restartHTTPS {
|
||||
go func() {
|
||||
globalContext.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||
m.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (m *tlsManager) validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
|
||||
if !setts.Enabled {
|
||||
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")
|
||||
}
|
||||
} 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")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !webCheckPortAvailable(setts.PortHTTPS) {
|
||||
return fmt.Errorf("port %d is not available, cannot enable HTTPS on it", setts.PortHTTPS)
|
||||
var (
|
||||
tlsConf tlsConfigSettings
|
||||
webAPIAddr netip.Addr
|
||||
webAPIPort uint16
|
||||
plainDNSPort uint16
|
||||
)
|
||||
|
||||
func() {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
tlsConf = config.TLS
|
||||
webAPIAddr = config.HTTPConfig.Address.Addr()
|
||||
webAPIPort = config.HTTPConfig.Address.Port()
|
||||
plainDNSPort = config.DNS.Port
|
||||
}()
|
||||
|
||||
err = validatePorts(
|
||||
tcpPort(webAPIPort),
|
||||
tcpPort(setts.PortHTTPS),
|
||||
tcpPort(setts.PortDNSOverTLS),
|
||||
tcpPort(setts.PortDNSCrypt),
|
||||
udpPort(plainDNSPort),
|
||||
udpPort(setts.PortDNSOverQUIC),
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return m.checkPortAvailability(tlsConf, setts.tlsConfigSettings, webAPIAddr)
|
||||
}
|
||||
|
||||
// validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home
|
||||
@@ -557,10 +636,11 @@ func validatePorts(
|
||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||
addPorts(
|
||||
tcpPorts,
|
||||
tcpPort(bindPort),
|
||||
tcpPort(dohPort),
|
||||
tcpPort(dotPort),
|
||||
tcpPort(dnscryptTCPPort),
|
||||
bindPort,
|
||||
dohPort,
|
||||
dotPort,
|
||||
dnscryptTCPPort,
|
||||
tcpPort(dnsPort),
|
||||
)
|
||||
|
||||
err = tcpPorts.Validate()
|
||||
@@ -569,7 +649,7 @@ func validatePorts(
|
||||
}
|
||||
|
||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||
addPorts(udpPorts, udpPort(dnsPort), udpPort(doqPort))
|
||||
addPorts(udpPorts, dnsPort, doqPort)
|
||||
|
||||
err = udpPorts.Validate()
|
||||
if err != nil {
|
||||
@@ -604,7 +684,7 @@ func (m *tlsManager) validateCertChain(
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: srvName,
|
||||
Roots: globalContext.tlsRoots,
|
||||
Roots: m.rootCerts,
|
||||
Intermediates: pool,
|
||||
}
|
||||
_, err = main.Verify(opts)
|
||||
@@ -615,6 +695,67 @@ func (m *tlsManager) validateCertChain(
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPortAvailability checks [tlsConfigSettings.PortHTTPS],
|
||||
// [tlsConfigSettings.PortDNSOverTLS], and [tlsConfigSettings.PortDNSOverQUIC]
|
||||
// are available for use. It checks the current configuration and, if needed,
|
||||
// attempts to bind to the port. The function returns human-readable error
|
||||
// messages for the frontend. This is best-effort check to prevent an "address
|
||||
// already in use" error.
|
||||
//
|
||||
// TODO(a.garipov): Adapt for HTTP/3.
|
||||
func (m *tlsManager) checkPortAvailability(
|
||||
currConf tlsConfigSettings,
|
||||
newConf tlsConfigSettings,
|
||||
addr netip.Addr,
|
||||
) (err error) {
|
||||
const (
|
||||
networkTCP = "tcp"
|
||||
networkUDP = "udp"
|
||||
|
||||
protoHTTPS = "HTTPS"
|
||||
protoDoT = "DNS-over-TLS"
|
||||
protoDoQ = "DNS-over-QUIC"
|
||||
)
|
||||
|
||||
needBindingCheck := []struct {
|
||||
network string
|
||||
proto string
|
||||
currPort uint16
|
||||
newPort uint16
|
||||
}{{
|
||||
network: networkTCP,
|
||||
proto: protoHTTPS,
|
||||
currPort: currConf.PortHTTPS,
|
||||
newPort: newConf.PortHTTPS,
|
||||
}, {
|
||||
network: networkTCP,
|
||||
proto: protoDoT,
|
||||
currPort: currConf.PortDNSOverTLS,
|
||||
newPort: newConf.PortDNSOverTLS,
|
||||
}, {
|
||||
network: networkUDP,
|
||||
proto: protoDoQ,
|
||||
currPort: currConf.PortDNSOverQUIC,
|
||||
newPort: newConf.PortDNSOverQUIC,
|
||||
}}
|
||||
|
||||
var errs []error
|
||||
for _, v := range needBindingCheck {
|
||||
port := v.newPort
|
||||
if v.currPort == port {
|
||||
continue
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(addr, port)
|
||||
err = aghnet.CheckPort(v.network, addrPort)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("port %d for %s is not available", port, v.proto))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// errNoIPInCert is the error that is returned from [tlsManager.parseCertChain]
|
||||
// if the leaf certificate doesn't contain IPs.
|
||||
const errNoIPInCert errors.Error = `certificates has no IP addresses; ` +
|
||||
@@ -718,27 +859,12 @@ func (m *tlsManager) validateCertificates(
|
||||
) (err error) {
|
||||
// Check only the public certificate separately from the key.
|
||||
if len(certChain) > 0 {
|
||||
var certs []*x509.Certificate
|
||||
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
|
||||
if !status.ValidCert {
|
||||
var ok bool
|
||||
ok, err = m.validateCertificate(ctx, status, certChain, serverName)
|
||||
if !ok {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
mainCert := certs[0]
|
||||
status.Subject = mainCert.Subject.String()
|
||||
status.Issuer = mainCert.Issuer.String()
|
||||
status.NotAfter = mainCert.NotAfter
|
||||
status.NotBefore = mainCert.NotBefore
|
||||
status.DNSNames = mainCert.DNSNames
|
||||
|
||||
if chainErr := m.validateCertChain(ctx, certs, serverName); chainErr != nil {
|
||||
// Let self-signed certs through and don't return this error to set
|
||||
// its message into the status.WarningValidation afterwards.
|
||||
err = chainErr
|
||||
} else {
|
||||
status.ValidChain = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the private key by parsing it.
|
||||
@@ -766,6 +892,41 @@ func (m *tlsManager) validateCertificates(
|
||||
return err
|
||||
}
|
||||
|
||||
// validateCertificate processes certificate data. status must not be nil, as
|
||||
// it is used to accumulate the validation results. Other parameters are
|
||||
// optional. If ok is true, the returned error, if any, is not critical.
|
||||
func (m *tlsManager) validateCertificate(
|
||||
ctx context.Context,
|
||||
status *tlsConfigStatus,
|
||||
certChain []byte,
|
||||
serverName string,
|
||||
) (ok bool, err error) {
|
||||
var certs []*x509.Certificate
|
||||
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
|
||||
if !status.ValidCert {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return false, err
|
||||
}
|
||||
|
||||
mainCert := certs[0]
|
||||
status.Subject = mainCert.Subject.String()
|
||||
status.Issuer = mainCert.Issuer.String()
|
||||
status.NotAfter = mainCert.NotAfter
|
||||
status.NotBefore = mainCert.NotBefore
|
||||
status.DNSNames = mainCert.DNSNames
|
||||
|
||||
err = m.validateCertChain(ctx, certs, serverName)
|
||||
if err != nil {
|
||||
// Let self-signed certs through and don't return this error to set
|
||||
// its message into the status.WarningValidation afterwards.
|
||||
return true, err
|
||||
}
|
||||
|
||||
status.ValidChain = true
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Key types.
|
||||
const (
|
||||
keyTypeECDSA = "ECDSA"
|
||||
@@ -828,17 +989,18 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if data.PrivateKey != "" {
|
||||
var key []byte
|
||||
key, err = base64.StdEncoding.DecodeString(data.PrivateKey)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
|
||||
}
|
||||
if data.PrivateKey == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
data.PrivateKey = string(key)
|
||||
if data.PrivateKeyPath != "" {
|
||||
return data, fmt.Errorf("private key data and file can't be set together")
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(data.PrivateKey)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
|
||||
}
|
||||
|
||||
data.PrivateKey = string(key)
|
||||
if data.PrivateKeyPath != "" {
|
||||
return data, fmt.Errorf("private key data and file can't be set together")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
Reference in New Issue
Block a user