Browse Source

DSL relay enhancements
- Add hot reload support for relay TLS configuration
- Add tactics override for DSL backend service address
- Fix broker RelayDSLRequest error recording
- Rename DSLRelayServiceURL

Rod Hynes 4 months ago
parent
commit
24af82e91d

+ 30 - 10
psiphon/common/dsl/dsl_test.go

@@ -22,9 +22,10 @@ package dsl
 import (
 	"bytes"
 	"context"
-	"crypto/x509"
 	"encoding/base64"
 	"encoding/hex"
+	"io/ioutil"
+	"os"
 	"runtime/debug"
 	"sync"
 	"testing"
@@ -127,6 +128,12 @@ var (
 
 func testDSLs(testConfig *testConfig) error {
 
+	testDataDirName, err := ioutil.TempDir("", "psiphon-dsl-test")
+	if err != nil {
+		return errors.Trace(err)
+	}
+	defer os.RemoveAll(testDataDirName)
+
 	// Initialize OSLs
 
 	var backendOSLPaveData1 []*osl.PaveData
@@ -143,7 +150,7 @@ func testDSLs(testConfig *testConfig) error {
 
 	// Initialize backend
 
-	tlsConfig, err := testutils.NewTestDSLRelayTLSConfig()
+	tlsConfig, err := testutils.NewTestDSLTLSConfig()
 	if err != nil {
 		return errors.Trace(err)
 	}
@@ -176,15 +183,25 @@ func testDSLs(testConfig *testConfig) error {
 
 	relayLogger := testutils.NewTestLoggerWithMetricValidator("relay", metricsValidator)
 
-	certPool := x509.NewCertPool()
-	certPool.AddCert(tlsConfig.CACertificate)
+	relayCACertificatesFilename,
+		relayHostCertificateFilename,
+		relayHostKeyFilename,
+		err := tlsConfig.WriteRelayFiles(testDataDirName)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	relayGetServiceAddress := func(_ common.GeoIPData) (string, error) {
+		return backend.GetAddress(), nil
+	}
 
 	relayConfig := &RelayConfig{
-		Logger:                      relayLogger,
-		CACertificates:              certPool,
-		HostCertificate:             tlsConfig.RelayCertificate,
-		DynamicServerListServiceURL: backend.GetAddress(),
-		HostID:                      testHostID,
+		Logger:                  relayLogger,
+		CACertificatesFilename:  relayCACertificatesFilename,
+		HostCertificateFilename: relayHostCertificateFilename,
+		HostKeyFilename:         relayHostKeyFilename,
+		GetServiceAddress:       relayGetServiceAddress,
+		HostID:                  testHostID,
 
 		APIParameterValidator: func(params common.APIParameters) error { return nil },
 
@@ -196,7 +213,10 @@ func testDSLs(testConfig *testConfig) error {
 		},
 	}
 
-	relay := NewRelay(relayConfig)
+	relay, err := NewRelay(relayConfig)
+	if err != nil {
+		return errors.Trace(err)
+	}
 
 	if !testConfig.cacheServerEntries {
 		relay.SetCacheParameters(0, 0)

+ 122 - 18
psiphon/common/dsl/relay.go

@@ -28,6 +28,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"os"
 	"sync"
 	"time"
 
@@ -51,18 +52,19 @@ const (
 
 // RelayConfig specifies the configuration for a Relay.
 //
-// The CACertificates and HostCertificate parameters are used for mutually
+// The CACertificates and HostCertificate/Key parameters are used for mutually
 // authenticated TLS between the Relay and the DSL backend. The HostID value
 // is sent to the DSL backend for logging, and should be populated with the
 // HostID in psiphond.config.
 type RelayConfig struct {
 	Logger common.Logger
 
-	CACertificates *x509.CertPool
+	CACertificatesFilename  string
+	HostCertificateFilename string
+	HostKeyFilename         string
 
-	HostCertificate *tls.Certificate
-
-	DynamicServerListServiceURL string
+	GetServiceAddress func(
+		clientGeoIPData common.GeoIPData) (string, error)
 
 	HostID string
 
@@ -85,10 +87,14 @@ type RelayConfig struct {
 // GetServerEntriesRequest requests may be fully or partially served out of
 // the local cache.
 type Relay struct {
-	config    *RelayConfig
-	tlsConfig *tls.Config
+	config *RelayConfig
+
+	caCertificatesFile  common.ReloadableFile
+	hostCertificateFile common.ReloadableFile
+	hostKeyFile         common.ReloadableFile
 
 	mutex                   sync.Mutex
+	tlsConfig               *tls.Config
 	httpClient              *http.Client
 	requestTimeout          time.Duration
 	requestRetryCount       int
@@ -98,16 +104,19 @@ type Relay struct {
 }
 
 // NewRelay creates a new Relay.
-func NewRelay(config *RelayConfig) *Relay {
+func NewRelay(config *RelayConfig) (*Relay, error) {
+
+	relay := &Relay{
+		config: config,
 
-	tlsConfig := &tls.Config{
-		RootCAs:      config.CACertificates,
-		Certificates: []tls.Certificate{*config.HostCertificate},
+		caCertificatesFile:  common.NewReloadableFile(config.CACertificatesFilename, false, nil),
+		hostCertificateFile: common.NewReloadableFile(config.HostCertificateFilename, false, nil),
+		hostKeyFile:         common.NewReloadableFile(config.HostKeyFilename, false, nil),
 	}
 
-	relay := &Relay{
-		config:    config,
-		tlsConfig: tlsConfig,
+	_, err := relay.Reload()
+	if err != nil {
+		return nil, errors.Trace(err)
 	}
 
 	relay.SetRequestParameters(
@@ -121,7 +130,98 @@ func NewRelay(config *RelayConfig) *Relay {
 		defaultServerEntryCacheTTL,
 		defaultServerEntryCacheMaxSize)
 
-	return relay
+	return relay, nil
+}
+
+// Reload reloads the TLS configuration when the file contents have changed.
+//
+// Reload implements the  common.Reloader interface.
+func (r *Relay) Reload() (bool, error) {
+
+	// The common.ReloadableFile.reloadAction callback not used; instead,
+	// ReloadableFiles are used to check for changed file contents. When any
+	// file has changed, all TLS configuration files are reloaded and the TLS
+	// configuration is reinitialized.
+
+	reloadedAny := false
+
+	reloaded, err := r.caCertificatesFile.Reload()
+	if err != nil {
+		return false, errors.Trace(err)
+	}
+	reloadedAny = reloadedAny || reloaded
+
+	reloaded, err = r.hostCertificateFile.Reload()
+	if err != nil {
+		return false, errors.Trace(err)
+	}
+	reloadedAny = reloadedAny || reloaded
+
+	reloaded, err = r.hostKeyFile.Reload()
+	if err != nil {
+		return false, errors.Trace(err)
+	}
+	reloadedAny = reloadedAny || reloaded
+
+	if !reloadedAny {
+		return false, nil
+	}
+
+	caCertsPEM, err := os.ReadFile(r.config.CACertificatesFilename)
+	if err != nil {
+		return false, errors.Trace(err)
+	}
+
+	caCertificates := x509.NewCertPool()
+	if !caCertificates.AppendCertsFromPEM(caCertsPEM) {
+		return false, errors.TraceNew("AppendCertsFromPEM failed")
+	}
+
+	hostCertificate, err := tls.LoadX509KeyPair(
+		r.config.HostCertificateFilename,
+		r.config.HostKeyFilename)
+	if err != nil {
+		return false, errors.Trace(err)
+	}
+
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	r.tlsConfig = &tls.Config{
+		RootCAs:      caCertificates,
+		Certificates: []tls.Certificate{hostCertificate},
+	}
+
+	if r.httpClient != nil {
+
+		// Replace the http.Client if it exists. See the comment in
+		// SetRequestParameters regarding in-flight requests and idle timeout
+		// limitations.
+
+		httpTransport := r.httpClient.Transport.(*http.Transport)
+
+		r.httpClient = &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig:     r.tlsConfig,
+				MaxConnsPerHost:     httpTransport.MaxConnsPerHost,
+				MaxIdleConns:        httpTransport.MaxIdleConns,
+				MaxIdleConnsPerHost: httpTransport.MaxIdleConnsPerHost,
+				IdleConnTimeout:     httpTransport.IdleConnTimeout,
+			},
+		}
+	}
+
+	return true, nil
+}
+
+// WillReload implements the common.Reloader interface.
+func (r *Relay) WillReload() bool {
+	return true
+}
+
+// ReloadLogDescription implements the common.Reloader interface.
+func (r *Relay) ReloadLogDescription() string {
+	return "DSL Relay TLS configuration"
 }
 
 // SetRequestParameters updates the HTTP request parameters used for upstream
@@ -144,7 +244,7 @@ func (r *Relay) SetRequestParameters(
 	// continue until complete and eventually the previous http.Client will
 	// be garbage collected.
 	//
-	// TODO: don't retain the previous http.Client as long as
+	// TODO: don't retain the previous http.Client for as long as
 	// http.Transport.IdleConnTimeout.
 
 	var httpTransport *http.Transport
@@ -166,7 +266,6 @@ func (r *Relay) SetRequestParameters(
 				IdleConnTimeout:     httpIdleConnTimeout,
 			},
 		}
-
 	}
 }
 
@@ -323,7 +422,12 @@ func (r *Relay) HandleRequest(
 			defer requestCancelFunc()
 		}
 
-		url := fmt.Sprintf("https://%s%s", r.config.DynamicServerListServiceURL, path)
+		serviceAddress, err := r.config.GetServiceAddress(clientGeoIPData)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+
+		url := fmt.Sprintf("https://%s%s", serviceAddress, path)
 
 		httpRequest, err := http.NewRequestWithContext(
 			requestCtx, "POST", url, bytes.NewBuffer(relayedRequest.Request))

+ 8 - 2
psiphon/common/inproxy/broker.go

@@ -1653,6 +1653,7 @@ func (b *Broker) handleClientDSL(
 
 	var logFields common.LogFields
 	var requestSize, responseSize int
+	var dslRelayErr error
 
 	// Always log the outcome.
 	defer func() {
@@ -1664,12 +1665,17 @@ func (b *Broker) handleClientDSL(
 		logFields["elapsed_time"] = time.Since(startTime) / time.Millisecond
 		logFields["request_size"] = requestSize
 		logFields["response_size"] = responseSize
+		loggedError := false
 		if retErr != nil {
 			logFields["error"] = retErr.Error()
+			loggedError = true
+		} else if dslRelayErr != nil {
+			logFields["error"] = dslRelayErr.Error()
+			loggedError = true
 		}
 		logFields.Add(transportLogFields)
 		b.config.Logger.LogMetric(brokerMetricName, logFields)
-		if retErr != nil {
+		if loggedError {
 			retErr = NewBrokerLoggedEvent(retErr)
 		}
 	}()
@@ -1694,7 +1700,7 @@ func (b *Broker) handleClientDSL(
 		// to send to the client, to ensure it retains its broker client
 		// round tripper. Any DSL relay errors, including missing
 		// configuration, will be logged to the broker_event.
-		retErr = err
+		dslRelayErr = errors.Trace(err)
 	}
 
 	responseSize = len(dslResponsePayload)

+ 2 - 0
psiphon/common/parameters/parameters.go

@@ -523,6 +523,7 @@ const (
 	CheckServerEntryTagsMaxWorkTime                    = "CheckServerEntryTagsMaxWorkTime"
 	ServerEntryPruneDialPortNumberZero                 = "ServerEntryPruneDialPortNumberZero"
 	CompressTactics                                    = "CompressTactics"
+	DSLRelayServiceAddress                             = "DSLRelayServiceAddress"
 	DSLRelayMaxHttpConns                               = "DSLRelayMaxHttpConns"
 	DSLRelayMaxHttpIdleConns                           = "DSLRelayMaxHttpIdleConns"
 	DSLRelayHttpIdleConnTimeout                        = "DSLRelayHttpIdleConnTimeout"
@@ -1149,6 +1150,7 @@ var defaultParameters = map[string]struct {
 
 	CompressTactics: {value: true},
 
+	DSLRelayServiceAddress:                            {value: "", flags: serverSideOnly},
 	DSLRelayMaxHttpConns:                              {value: 100, minimum: 1, flags: serverSideOnly},
 	DSLRelayMaxHttpIdleConns:                          {value: 10, minimum: 1, flags: serverSideOnly},
 	DSLRelayHttpIdleConnTimeout:                       {value: 120 * time.Second, minimum: time.Duration(0), flags: serverSideOnly},

+ 11 - 6
psiphon/common/reloader.go

@@ -45,9 +45,9 @@ type Reloader interface {
 	// WillReload indicates if the data object is capable of reloading.
 	WillReload() bool
 
-	// LogDescription returns a description to be used for logging
+	// ReloadLogDescription returns a description to be used for logging
 	// events related to the Reloader.
-	LogDescription() string
+	ReloadLogDescription() string
 }
 
 // ReloadableFile is a file-backed Reloader. This type is intended to be embedded
@@ -77,6 +77,9 @@ type ReloadableFile struct {
 // reloadAction; otherwise, reloadAction receives a nil argument and is
 // responsible for loading the file. The latter option allows for cases where
 // the file contents must be streamed, memory mapped, etc.
+//
+// The returned ReloadableFile contains a sync.RWMutex and must not be copied
+// after first use.
 func NewReloadableFile(
 	filename string,
 	loadFileContent bool,
@@ -177,9 +180,11 @@ func (reloadable *ReloadableFile) Reload() (bool, error) {
 	reloadable.Lock()
 	defer reloadable.Unlock()
 
-	err = reloadable.reloadAction(content, fileModTime)
-	if err != nil {
-		return false, errors.Trace(err)
+	if reloadable.reloadAction != nil {
+		err := reloadable.reloadAction(content, fileModTime)
+		if err != nil {
+			return false, errors.Trace(err)
+		}
 	}
 
 	reloadable.hasLoaded = true
@@ -188,6 +193,6 @@ func (reloadable *ReloadableFile) Reload() (bool, error) {
 	return true, nil
 }
 
-func (reloadable *ReloadableFile) LogDescription() string {
+func (reloadable *ReloadableFile) ReloadLogDescription() string {
 	return reloadable.filename
 }

+ 46 - 5
psiphon/internal/testutils/dsl.go

@@ -34,6 +34,8 @@ import (
 	"math/big"
 	"net"
 	"net/http"
+	"os"
+	"path/filepath"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -110,7 +112,7 @@ type DSLBackendTestShim interface {
 // TestDSLBackend is a mock DSL backend intended only for testing.
 type TestDSLBackend struct {
 	shim                    DSLBackendTestShim
-	tlsConfig               *TestDSLRelayTLSConfig
+	tlsConfig               *TestDSLTLSConfig
 	expectedClientIP        string
 	expectedClientGeoIPData *common.GeoIPData
 	expectedHostID          string
@@ -127,7 +129,7 @@ type dslSourcedServerEntry struct {
 
 func NewTestDSLBackend(
 	shim DSLBackendTestShim,
-	tlsConfig *TestDSLRelayTLSConfig,
+	tlsConfig *TestDSLTLSConfig,
 	expectedClientIP string,
 	expectedClientGeoIPData *common.GeoIPData,
 	expectedHostID string,
@@ -724,7 +726,7 @@ func InitializeTestOSLPaveData() ([]*osl.PaveData, []*osl.PaveData, []*osl.SLOK,
 	return backendPaveData1, backendPaveData2, clientSLOKs, nil
 }
 
-type TestDSLRelayTLSConfig struct {
+type TestDSLTLSConfig struct {
 	CACertificate         *x509.Certificate
 	CACertificatePEM      []byte
 	BackendCertificate    *tls.Certificate
@@ -735,7 +737,7 @@ type TestDSLRelayTLSConfig struct {
 	RelayKeyPEM           []byte
 }
 
-func NewTestDSLRelayTLSConfig() (*TestDSLRelayTLSConfig, error) {
+func NewTestDSLTLSConfig() (*TestDSLTLSConfig, error) {
 
 	CAPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
@@ -830,7 +832,7 @@ func NewTestDSLRelayTLSConfig() (*TestDSLRelayTLSConfig, error) {
 		return nil, errors.Trace(err)
 	}
 
-	return &TestDSLRelayTLSConfig{
+	return &TestDSLTLSConfig{
 		CACertificate:         CACertificate,
 		CACertificatePEM:      CACertificatePEM,
 		BackendCertificate:    backendCertificate,
@@ -841,3 +843,42 @@ func NewTestDSLRelayTLSConfig() (*TestDSLRelayTLSConfig, error) {
 		RelayKeyPEM:           relayKeyPEM,
 	}, nil
 }
+
+func (config *TestDSLTLSConfig) WriteRelayFiles(dirName string) (
+	string, string, string, error) {
+
+	caCertificatesFilename := filepath.Join(
+		dirName, "dslRelayCACert.pem")
+	err := os.WriteFile(
+		caCertificatesFilename,
+		config.CACertificatePEM,
+		0644)
+	if err != nil {
+		return "", "", "", errors.Trace(err)
+	}
+
+	hostCertificateFilename := filepath.Join(
+		dirName, "dslRelayHostCert.pem")
+	err = os.WriteFile(
+		hostCertificateFilename,
+		config.RelayCertificatePEM,
+		0644)
+	if err != nil {
+		return "", "", "", errors.Trace(err)
+	}
+
+	hostKeyFilename := filepath.Join(
+		dirName, "dslRelayHostKey.pem")
+	err = os.WriteFile(
+		hostKeyFilename,
+		config.RelayKeyPEM,
+		0644)
+	if err != nil {
+		return "", "", "", errors.Trace(err)
+	}
+
+	return caCertificatesFilename,
+		hostCertificateFilename,
+		hostKeyFilename,
+		nil
+}

+ 3 - 26
psiphon/server/config.go

@@ -22,7 +22,6 @@ package server
 import (
 	"crypto/rand"
 	"crypto/rsa"
-	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/hex"
@@ -560,10 +559,10 @@ type Config struct {
 	// values.
 	IptablesAcceptRateLimitTunnelProtocolRateLimits map[string][2]int `json:",omitempty"`
 
-	// DSLRelayServiceURL specifies the DSL backend address to use for
+	// DSLRelayServiceAddress specifies the DSL backend address to use for
 	// relaying client DSL requests. When specified, the following DSL relay
 	// PKI parameters must also be specified.
-	DSLRelayServiceURL string `json:",omitempty"`
+	DSLRelayServiceAddress string `json:",omitempty"`
 
 	// DSLRelayCACertificatesFilename is part of the mutual authentication PKI
 	// for DSL relaying.
@@ -589,8 +588,6 @@ type Config struct {
 	region                                         string
 	runningProtocols                               []string
 	runningOnlyInproxyBroker                       bool
-	dslRelayCACertificates                         *x509.CertPool
-	dslRelayHostCertificate                        *tls.Certificate
 }
 
 // GetLogFileReopenConfig gets the reopen retries, and create/mode inputs for
@@ -898,33 +895,13 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		}
 	}
 
-	if config.DSLRelayServiceURL != "" {
+	if config.DSLRelayServiceAddress != "" {
 		if config.DSLRelayCACertificatesFilename == "" ||
 			config.DSLRelayHostCertificateFilename == "" ||
 			config.DSLRelayHostKeyFilename == "" {
 			return nil, errors.TraceNew(
 				"DSL relay requires mutual TLS configuration")
 		}
-
-		caCertsPEM, err := os.ReadFile(config.DSLRelayCACertificatesFilename)
-		if err != nil {
-			return nil, errors.Trace(err)
-		}
-
-		dslRelayCACertificates := x509.NewCertPool()
-		if !dslRelayCACertificates.AppendCertsFromPEM(caCertsPEM) {
-			return nil, errors.TraceNew("AppendCertsFromPEM failed")
-		}
-
-		dslRelayHostCertificate, err := tls.LoadX509KeyPair(
-			config.DSLRelayHostCertificateFilename,
-			config.DSLRelayHostKeyFilename)
-		if err != nil {
-			return nil, errors.Trace(err)
-		}
-
-		config.dslRelayCACertificates = dslRelayCACertificates
-		config.dslRelayHostCertificate = &dslRelayHostCertificate
 	}
 
 	// Limitation: the following is a shortcut which extracts the server's

+ 27 - 0
psiphon/server/dsl.go

@@ -29,6 +29,33 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 )
 
+func dslMakeGetServiceAddress(
+	support *SupportServices) func(common.GeoIPData) (string, error) {
+
+	return func(clientGeoIPData common.GeoIPData) (string, error) {
+
+		// Defaults to the config value, unless a non-blank address is
+		// configured in tactics.
+
+		address := support.Config.DSLRelayServiceAddress
+
+		p, err := support.ServerTacticsParametersCache.Get(GeoIPData(clientGeoIPData))
+		if err != nil {
+			return "", errors.Trace(err)
+		}
+		defer p.Close()
+		if p.IsNil() {
+			return address, nil
+		}
+		tacticsAddress := p.String(parameters.DSLRelayServiceAddress)
+		if tacticsAddress == "" {
+			return address, nil
+		}
+
+		return tacticsAddress, nil
+	}
+}
+
 func dslReloadRelayTactics(support *SupportServices) error {
 
 	// Assumes no GeoIP targeting for DSL relay tactics.

+ 5 - 27
psiphon/server/server_test.go

@@ -1246,7 +1246,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 	if doDSL {
 
-		serverConfig["DSLRelayServiceURL"] = dslTestConfig.backend.GetAddress()
+		serverConfig["DSLRelayServiceAddress"] = dslTestConfig.backend.GetAddress()
 		serverConfig["DSLRelayCACertificatesFilename"] = dslTestConfig.relayCACertificatesFilename
 		serverConfig["DSLRelayHostCertificateFilename"] = dslTestConfig.relayHostCertificateFilename
 		serverConfig["DSLRelayHostKeyFilename"] = dslTestConfig.relayHostKeyFilename
@@ -4829,7 +4829,7 @@ func generateInproxyTestConfig(
 }
 
 type dslTestConfig struct {
-	relayTLSConfig               *testutils.TestDSLRelayTLSConfig
+	relayTLSConfig               *testutils.TestDSLTLSConfig
 	relayCACertificatesFilename  string
 	relayHostCertificateFilename string
 	relayHostKeyFilename         string
@@ -4840,37 +4840,15 @@ type dslTestConfig struct {
 
 func generateDSLTestConfig() (*dslTestConfig, error) {
 
-	relayTLSConfig, err := testutils.NewTestDSLRelayTLSConfig()
+	relayTLSConfig, err := testutils.NewTestDSLTLSConfig()
 	if err != nil {
 		return nil, errors.Trace(err)
 	}
 
-	relayCACertificatesFilename := filepath.Join(
-		testDataDirName, "dslRelayCACert.pem")
-	err = os.WriteFile(
-		relayCACertificatesFilename,
-		relayTLSConfig.CACertificatePEM,
-		0644)
-	if err != nil {
-		return nil, errors.Trace(err)
-	}
-
-	relayHostCertificateFilename := filepath.Join(
-		testDataDirName, "dslRelayHostCert.pem")
-	err = os.WriteFile(
+	relayCACertificatesFilename,
 		relayHostCertificateFilename,
-		relayTLSConfig.RelayCertificatePEM,
-		0644)
-	if err != nil {
-		return nil, errors.Trace(err)
-	}
-
-	relayHostKeyFilename := filepath.Join(
-		testDataDirName, "dslRelayHostKey.pem")
-	err = os.WriteFile(
 		relayHostKeyFilename,
-		relayTLSConfig.RelayKeyPEM,
-		0644)
+		err := relayTLSConfig.WriteRelayFiles(testDataDirName)
 	if err != nil {
 		return nil, errors.Trace(err)
 	}

+ 15 - 7
psiphon/server/services.go

@@ -149,16 +149,20 @@ func RunServices(configJSON []byte) (retErr error) {
 		support.PacketManipulator = packetManipulator
 	}
 
-	if config.DSLRelayServiceURL != "" {
-		support.dslRelay = dsl.NewRelay(&dsl.RelayConfig{
+	if config.DSLRelayServiceAddress != "" {
+		support.dslRelay, err = dsl.NewRelay(&dsl.RelayConfig{
 			Logger:                        CommonLogger(log),
-			CACertificates:                config.dslRelayCACertificates,
-			HostCertificate:               config.dslRelayHostCertificate,
-			DynamicServerListServiceURL:   config.DSLRelayServiceURL,
+			CACertificatesFilename:        config.DSLRelayCACertificatesFilename,
+			HostCertificateFilename:       config.DSLRelayHostCertificateFilename,
+			HostKeyFilename:               config.DSLRelayHostKeyFilename,
+			GetServiceAddress:             dslMakeGetServiceAddress(support),
 			HostID:                        config.HostID,
 			APIParameterValidator:         getDSLAPIParameterValidator(config),
 			APIParameterLogFieldFormatter: getDSLAPIParameterLogFieldFormatter(),
 		})
+		if err != nil {
+			return errors.Trace(err)
+		}
 
 		err := dslReloadRelayTactics(support)
 		if err != nil {
@@ -690,6 +694,10 @@ func (support *SupportServices) Reload() {
 			support.Blocklist},
 		support.GeoIPService.Reloaders()...)
 
+	if support.dslRelay != nil {
+		reloaders = append(reloaders, support.dslRelay)
+	}
+
 	reloadDiscovery := func(reloadedTactics bool) {
 		err := support.discovery.reload(reloadedTactics)
 		if err != nil {
@@ -772,13 +780,13 @@ func (support *SupportServices) Reload() {
 		if err != nil {
 			log.WithTraceFields(
 				LogFields{
-					"reloader": reloader.LogDescription(),
+					"reloader": reloader.ReloadLogDescription(),
 					"error":    err}).Error("reload failed")
 			// Keep running with previous state
 		} else {
 			log.WithTraceFields(
 				LogFields{
-					"reloader": reloader.LogDescription(),
+					"reloader": reloader.ReloadLogDescription(),
 					"reloaded": reloaded}).Info("reload success")
 		}
 	}