* TLS is now a separate module (logically)

This commit is contained in:
Simon Zolin 2020-02-19 15:28:06 +03:00
parent 8e4bc29103
commit db30f27c8f
7 changed files with 267 additions and 184 deletions

View File

@ -71,7 +71,6 @@ Contents:
![](doc/agh-arch.png)
## First startup
The first application startup is detected when there's no .yaml configuration file.

View File

@ -50,8 +50,8 @@ type configuration struct {
// An active session is automatically refreshed once a day.
WebSessionTTLHours uint32 `yaml:"web_session_ttl"`
DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"`
DNS dnsConfig `yaml:"dns"`
TLS tlsConfigSettings `yaml:"tls"`
Filters []filter `yaml:"filters"`
WhitelistFilters []filter `yaml:"whitelist_filters"`
@ -101,33 +101,6 @@ type tlsConfigSettings struct {
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
}
// field ordering is not important -- these are for API and are recalculated on each run
type tlsConfigStatus struct {
ValidCert bool `yaml:"-" json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
ValidChain bool `yaml:"-" json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
Subject string `yaml:"-" json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
Issuer string `yaml:"-" json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
NotBefore time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
NotAfter time.Time `yaml:"-" json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
DNSNames []string `yaml:"-" json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
// key status
ValidKey bool `yaml:"-" json:"valid_key"` // ValidKey is true if the key is a valid private key
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator
ValidPair bool `yaml:"-" json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
// warnings
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
}
// field ordering is important -- yaml fields will mirror ordering from here
type tlsConfig struct {
tlsConfigSettings `yaml:",inline" json:",inline"`
tlsConfigStatus `yaml:"-" json:",inline"`
}
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
BindPort: 3000,
@ -147,11 +120,9 @@ var config = configuration{
FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24,
},
TLS: tlsConfig{
tlsConfigSettings: tlsConfigSettings{
PortHTTPS: 443,
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
},
TLS: tlsConfigSettings{
PortHTTPS: 443,
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
},
DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400,
@ -225,12 +196,6 @@ func parseConfig() error {
config.DNS.FiltersUpdateIntervalHours = 24
}
status := tlsConfigStatus{}
if !tlsLoadConfig(&config.TLS, &status) {
log.Error("%s", status.WarningValidation)
return err
}
return nil
}
@ -259,6 +224,11 @@ func (c *configuration) write() error {
if Context.auth != nil {
config.Users = Context.auth.GetUsers()
}
if Context.tls != nil {
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
config.TLS = tlsConf
}
if Context.stats != nil {
sdc := stats.DiskConfig{}
@ -308,13 +278,3 @@ func (c *configuration) write() error {
return nil
}
func writeAllConfigs() error {
err := config.write()
if err != nil {
log.Error("Couldn't write config: %s", err)
return err
}
return nil
}

View File

@ -26,9 +26,6 @@ func returnOK(w http.ResponseWriter) {
}
}
func httpOK(r *http.Request, w http.ResponseWriter) {
}
func httpError(w http.ResponseWriter, code int, format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Info(text)
@ -38,15 +35,6 @@ func httpError(w http.ResponseWriter, code int, format string, args ...interface
// ---------------
// dns run control
// ---------------
func writeAllConfigsAndReloadDNS() error {
err := writeAllConfigs()
if err != nil {
log.Error("Couldn't write all configs: %s", err)
return err
}
return reconfigureDNSServer()
}
func addDNSAddress(dnsAddresses *[]string, addr string) {
if config.DNS.Port != 53 {
addr = fmt.Sprintf("%s:%d", addr, config.DNS.Port)
@ -143,23 +131,6 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(data)
}
// --------------
// DNS-over-HTTPS
// --------------
func handleDOH(w http.ResponseWriter, r *http.Request) {
if !config.TLS.AllowUnencryptedDOH && r.TLS == nil {
httpError(w, http.StatusNotFound, "Not Found")
return
}
if !isRunning() {
httpError(w, http.StatusInternalServerError, "DNS server is not running")
return
}
Context.dnsServer.ServeHTTP(w, r)
}
// ------------------------
// registration of handlers
// ------------------------
@ -171,8 +142,6 @@ func registerControlHandlers() {
httpRegister(http.MethodPost, "/control/update", handleUpdate)
httpRegister("GET", "/control/profile", handleGetProfile)
RegisterTLSHandlers()
RegisterAuthHandlers()
http.HandleFunc("/dns-query", postInstall(handleDOH))
@ -265,7 +234,7 @@ func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.Res
}
// enforce https?
if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && Context.web.httpsServer.server != nil {
if r.TLS == nil && Context.web.forceHTTPS && Context.web.httpsServer.server != nil {
// yes, and we want host from host:port
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
@ -275,7 +244,7 @@ func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.Res
// construct new URL to redirect to
newURL := url.URL{
Scheme: "https",
Host: net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
Host: net.JoinHostPort(host, strconv.Itoa(Context.web.portHTTPS)),
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}

View File

@ -35,7 +35,7 @@ type netInterfaceJSON struct {
}
// Get initial installation settings
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{}
data.WebPort = 80
data.DNSPort = 53
@ -93,7 +93,7 @@ type checkConfigResp struct {
}
// Check if ports are available, respond with results
func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
reqData := checkConfigReq{}
respData := checkConfigResp{}
err := json.NewDecoder(r.Body).Decode(&reqData)
@ -275,7 +275,7 @@ func copyInstallSettings(dst *configuration, src *configuration) {
}
// Apply new configuration, start DNS server, restart Web server
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
newSettings := applyConfigReq{}
err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil {
@ -325,22 +325,11 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
config.DNS.BindHost = newSettings.DNS.IP
config.DNS.Port = newSettings.DNS.Port
err = initDNSServer()
var err2 error
if err == nil {
err2 = startDNSServer()
if err2 != nil {
closeDNSServer()
}
}
if err != nil || err2 != nil {
err = StartMods()
if err != nil {
Context.firstRun = true
copyInstallSettings(&config, &curConfig)
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't initialize DNS server: %s", err)
} else {
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err2)
}
httpError(w, http.StatusInternalServerError, "%s", err)
return
}
@ -369,8 +358,8 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
returnOK(w)
}
func registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
func (web *Web) registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))
}

View File

@ -156,11 +156,17 @@ func generateServerConfig() dnsforward.ServerConfig {
OnDNSRequest: onDNSRequest,
}
if config.TLS.Enabled {
newconfig.TLSConfig = config.TLS.TLSConfig
if config.TLS.PortDNSOverTLS != 0 {
newconfig.TLSListenAddr = &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.TLS.PortDNSOverTLS}
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
if tlsConf.Enabled {
newconfig.TLSConfig = tlsConf.TLSConfig
if tlsConf.PortDNSOverTLS != 0 {
newconfig.TLSListenAddr = &net.TCPAddr{
IP: net.ParseIP(config.DNS.BindHost),
Port: tlsConf.PortDNSOverTLS,
}
}
newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH
}
newconfig.TLSv12Roots = Context.tlsRoots

View File

@ -65,8 +65,9 @@ type homeContext struct {
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
dhcpServer *dhcpd.Server // DHCP module
auth *Auth // HTTP authentication module
filters Filtering
web *Web
filters Filtering // DNS filtering module
web *Web // Web (HTTP, HTTPS) module
tls *TLSMod // TLS module
// Runtime properties
// --
@ -119,6 +120,7 @@ func Main(version string, channel string, armVer string) {
switch sig {
case syscall.SIGHUP:
Context.clients.Reload()
Context.tls.Reload()
default:
cleanup()
@ -247,11 +249,15 @@ func run(args options) {
}
config.Users = nil
Context.tls = tlsCreate(config.TLS)
if Context.tls == nil {
log.Fatalf("Can't initialize TLS module")
}
webConf := WebConfig{
firstRun: Context.firstRun,
BindHost: config.BindHost,
BindPort: config.BindPort,
TLS: config.TLS,
}
Context.web = CreateWeb(&webConf)
if Context.web == nil {
@ -263,6 +269,8 @@ func run(args options) {
if err != nil {
log.Fatalf("%s", err)
}
Context.tls.Start()
go func() {
err := startDNSServer()
if err != nil {
@ -282,6 +290,23 @@ func run(args options) {
select {}
}
// StartMods - initialize and start DNS after installation
func StartMods() error {
err := initDNSServer()
if err != nil {
return err
}
Context.tls.Start()
err = startDNSServer()
if err != nil {
closeDNSServer()
return err
}
return nil
}
// Check if the current user has root (administrator) rights
// and if not, ask and try to run as root
func requireAdminRights() {
@ -408,6 +433,11 @@ func cleanup() {
if err != nil {
log.Error("Couldn't stop DHCP server: %s", err)
}
if Context.tls != nil {
Context.tls.Close()
Context.tls = nil
}
}
// This function is called before application exits
@ -528,11 +558,13 @@ func loadOptions() options {
func printHTTPAddresses(proto string) {
var address string
if proto == "https" && config.TLS.ServerName != "" {
if config.TLS.PortHTTPS == 443 {
log.Printf("Go to https://%s", config.TLS.ServerName)
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
if proto == "https" && tlsConf.ServerName != "" {
if tlsConf.PortHTTPS == 443 {
log.Printf("Go to https://%s", tlsConf.ServerName)
} else {
log.Printf("Go to https://%s:%d", config.TLS.ServerName, config.TLS.PortHTTPS)
log.Printf("Go to https://%s:%d", tlsConf.ServerName, tlsConf.PortHTTPS)
}
} else if config.BindHost == "0.0.0.0" {
log.Println("AdGuard Home is available on the following addresses:")

View File

@ -1,9 +1,6 @@
// Control: TLS configuring handlers
package home
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
@ -16,18 +13,125 @@ import (
"fmt"
"io/ioutil"
"net/http"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
var tlsWebHandlersRegistered = false
// TLSMod - TLS module object
type TLSMod struct {
certLastMod time.Time // last modification time of the certificate file
conf tlsConfigSettings
confLock sync.Mutex
status tlsConfigStatus
}
// Create TLS module
func tlsCreate(conf tlsConfigSettings) *TLSMod {
t := &TLSMod{}
t.conf = conf
if t.conf.Enabled {
if !t.load() {
return nil
}
t.setCertFileTime()
}
return t
}
func (t *TLSMod) load() bool {
if !tlsLoadConfig(&t.conf, &t.status) {
return false
}
// validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(string(t.conf.CertificateChainData), string(t.conf.PrivateKeyData), t.conf.ServerName)
if !data.ValidPair {
log.Error(data.WarningValidation)
return false
}
t.status = data
return true
}
// Close - close module
func (t *TLSMod) Close() {
}
// WriteDiskConfig - write config
func (t *TLSMod) WriteDiskConfig(conf *tlsConfigSettings) {
t.confLock.Lock()
*conf = t.conf
t.confLock.Unlock()
}
func (t *TLSMod) setCertFileTime() {
if len(t.conf.CertificatePath) == 0 {
return
}
fi, err := os.Stat(t.conf.CertificatePath)
if err != nil {
log.Error("TLS: %s", err)
return
}
t.certLastMod = fi.ModTime().UTC()
}
// Start - start the module
func (t *TLSMod) Start() {
if !tlsWebHandlersRegistered {
tlsWebHandlersRegistered = true
t.registerWebHandlers()
}
t.confLock.Lock()
tlsConf := t.conf
t.confLock.Unlock()
Context.web.TLSConfigChanged(tlsConf)
}
// Reload - reload certificate file
func (t *TLSMod) Reload() {
t.confLock.Lock()
tlsConf := t.conf
t.confLock.Unlock()
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
return
}
fi, err := os.Stat(tlsConf.CertificatePath)
if err != nil {
log.Error("TLS: %s", err)
return
}
if fi.ModTime().UTC().Equal(t.certLastMod) {
log.Debug("TLS: certificate file isn't modified")
return
}
log.Debug("TLS: certificate file is modified")
t.confLock.Lock()
r := t.load()
t.confLock.Unlock()
if !r {
return
}
t.certLastMod = fi.ModTime().UTC()
_ = reconfigureDNSServer()
Context.web.TLSConfigChanged(tlsConf)
}
// Set certificate and private key data
func tlsLoadConfig(tls *tlsConfig, status *tlsConfigStatus) bool {
func tlsLoadConfig(tls *tlsConfigSettings, status *tlsConfigStatus) bool {
tls.CertificateChainData = []byte(tls.CertificateChain)
tls.PrivateKeyData = []byte(tls.PrivateKey)
@ -61,98 +165,115 @@ func tlsLoadConfig(tls *tlsConfig, status *tlsConfigStatus) bool {
return true
}
// RegisterTLSHandlers registers HTTP handlers for TLS configuration
func RegisterTLSHandlers() {
httpRegister(http.MethodGet, "/control/tls/status", handleTLSStatus)
httpRegister(http.MethodPost, "/control/tls/configure", handleTLSConfigure)
httpRegister(http.MethodPost, "/control/tls/validate", handleTLSValidate)
type tlsConfigStatus struct {
ValidCert bool `json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
ValidChain bool `json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
Subject string `json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
Issuer string `json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
NotBefore time.Time `json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
NotAfter time.Time `json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
DNSNames []string `json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
// key status
ValidKey bool `json:"valid_key"` // ValidKey is true if the key is a valid private key
KeyType string `json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator
ValidPair bool `json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
// warnings
WarningValidation string `json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
}
func handleTLSStatus(w http.ResponseWriter, r *http.Request) {
marshalTLS(w, config.TLS)
// field ordering is important -- yaml fields will mirror ordering from here
type tlsConfig struct {
tlsConfigSettings `json:",inline"`
tlsConfigStatus `json:",inline"`
}
func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
func (t *TLSMod) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
t.confLock.Lock()
data := tlsConfig{
tlsConfigSettings: t.conf,
tlsConfigStatus: t.status,
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if Context.web.httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
status := tlsConfigStatus{}
if tlsLoadConfig(&data, &status) {
status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
}
data.tlsConfigStatus = status
t.confLock.Unlock()
marshalTLS(w, data)
}
func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
setts, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
if !WebCheckPortAvailable(setts.PortHTTPS) {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", setts.PortHTTPS)
return
}
status := tlsConfigStatus{}
if tlsLoadConfig(&setts, &status) {
status = validateCertificates(string(setts.CertificateChainData), string(setts.PrivateKeyData), setts.ServerName)
}
data := tlsConfig{
tlsConfigSettings: setts,
tlsConfigStatus: status,
}
marshalTLS(w, data)
}
func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if Context.web.httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
if !WebCheckPortAvailable(data.PortHTTPS) {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
status := tlsConfigStatus{}
if !tlsLoadConfig(&data, &status) {
data.tlsConfigStatus = status
marshalTLS(w, data)
data2 := tlsConfig{
tlsConfigSettings: data,
tlsConfigStatus: t.status,
}
marshalTLS(w, data2)
return
}
data.tlsConfigStatus = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
restartHTTPS := false
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
t.confLock.Lock()
if !reflect.DeepEqual(t.conf, data) {
log.Printf("tls config settings have changed, will restart HTTPS server")
restartHTTPS = true
}
config.TLS = data
err = writeAllConfigsAndReloadDNS()
t.conf = data
t.status = status
t.confLock.Unlock()
t.setCertFileTime()
onConfigModified()
err = reconfigureDNSServer()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
httpError(w, http.StatusInternalServerError, "%s", err)
return
}
marshalTLS(w, data)
data2 := tlsConfig{
tlsConfigSettings: data,
tlsConfigStatus: t.status,
}
marshalTLS(w, data2)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTPS {
go func() {
time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
Context.web.httpsServer.cond.L.Lock()
Context.web.httpsServer.cond.Broadcast()
if Context.web.httpsServer.server != nil {
Context.web.httpsServer.server.Shutdown(context.TODO())
}
Context.web.httpsServer.cond.L.Unlock()
Context.web.TLSConfigChanged(data)
}()
}
}
@ -337,8 +458,8 @@ func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
}
// unmarshalTLS handles base64-encoded certificates transparently
func unmarshalTLS(r *http.Request) (tlsConfig, error) {
data := tlsConfig{}
func unmarshalTLS(r *http.Request) (tlsConfigSettings, error) {
data := tlsConfigSettings{}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
return data, errorx.Decorate(err, "Failed to parse new TLS config json")
@ -389,3 +510,10 @@ func marshalTLS(w http.ResponseWriter, data tlsConfig) {
return
}
}
// registerWebHandlers registers HTTP handlers for TLS configuration
func (t *TLSMod) registerWebHandlers() {
httpRegister("GET", "/control/tls/status", t.handleTLSStatus)
httpRegister("POST", "/control/tls/configure", t.handleTLSConfigure)
httpRegister("POST", "/control/tls/validate", t.handleTLSValidate)
}