diff --git a/.gitignore b/.gitignore index fbf486ab6..dc860b05c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,8 @@ dnsproxy.exe example.crt example.key coverage.txt -config.yaml \ No newline at end of file +config.yaml +client.crt +client.key +tlsclient.crt +tlsclient.key diff --git a/README.md b/README.md index b531677a6..143fd11c5 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Application Options: --version Prints the program version Help Options: - -h, --help Show this help message + -h, --help Show this help message ``` ## Examples diff --git a/main.go b/main.go index ecc3bdd4c..20504c32a 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,14 @@ type Options struct { // Upstream DNS servers settings // -- + // DoH Upstream Authentication + + // Path to the .crt with the client-side certificate for upstream client authentication + TLSClientCertPath string `yaml:"tls-client-crt" long:"tls-client-crt" description:"Path to the file with the TLS certificate used for TLS client authentication (supported by DoH/DoT/DoQ)"` + + // Path to the file with the client-side private key for upstream client authentication + TLSClientKeyPath string `yaml:"tls-client-key" long:"tls-client-key" description:"Path to the file with the TLS certificate used for TLS client authentication (supported by DoH/DoT/DoQ)"` + // DNS upstreams Upstreams []string `yaml:"upstream" short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times). You can also specify path to a file with the list of servers" optional:"false"` @@ -278,6 +286,7 @@ func createProxyConfig(options *Options) proxy.Config { MaxGoroutines: options.MaxGoRoutines, } + initTLSClient(&config, options) initUpstreams(&config, options) initEDNS(&config, options) initBogusNXDomain(&config, options) @@ -288,14 +297,14 @@ func createProxyConfig(options *Options) proxy.Config { return config } -// initUpstreams inits upstream-related config func initUpstreams(config *proxy.Config, options *Options) { // Init upstreams upstreams := loadServersList(options.Upstreams) upsOpts := &upstream.Options{ - InsecureSkipVerify: options.Insecure, - Bootstrap: options.BootstrapDNS, - Timeout: defaultTimeout, + InsecureSkipVerify: options.Insecure, + Bootstrap: options.BootstrapDNS, + Timeout: defaultTimeout, + TLSClientCertificates: config.TLSClientCertificates, } upstreamConfig, err := proxy.ParseUpstreamsConfig(upstreams, upsOpts) if err != nil { @@ -374,6 +383,18 @@ func initTLSConfig(config *proxy.Config, options *Options) { } } +// initTLSConfig inits the DoH Client Auth TLS config +func initTLSClient(config *proxy.Config, options *Options) { + if options.TLSClientCertPath != "" && options.TLSClientKeyPath != "" { + cert, err := loadX509KeyPair(options.TLSClientCertPath, options.TLSClientKeyPath) + if err != nil { + log.Fatalf("could not load TLS cert for TLS client authentication: %s", err) + return + } + config.TLSClientCertificates = &cert + } +} + // initDNSCryptConfig inits the DNSCrypt config func initDNSCryptConfig(config *proxy.Config, options *Options) { if options.DNSCryptConfigPath == "" { @@ -539,9 +560,8 @@ func newTLSConfig(options *Options) (*tls.Config, error) { cert, err := loadX509KeyPair(options.TLSCertPath, options.TLSKeyPath) if err != nil { - return nil, fmt.Errorf("could not load TLS cert: %s", err) + return nil, fmt.Errorf("could not load TLS cert for TLS server: %s", err) } - return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: uint16(tlsMinVersion), MaxVersion: uint16(tlsMaxVersion)}, nil } diff --git a/proxy/config.go b/proxy/config.go index 11185434c..bceb331c2 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -53,9 +53,10 @@ type Config struct { // Encryption configuration // -- - TLSConfig *tls.Config // necessary for TLS, HTTPS, QUIC - DNSCryptProviderName string // DNSCrypt provider name - DNSCryptResolverCert *dnscrypt.Cert // DNSCrypt resolver certificate + TLSConfig *tls.Config // necessary for TLS, HTTPS, QUIC + TLSClientCertificates *tls.Certificate // necessary for DoH/DoT/DoQ Client Authentication + DNSCryptProviderName string // DNSCrypt provider name + DNSCryptResolverCert *dnscrypt.Cert // DNSCrypt resolver certificate // Rate-limiting and anti-DNS amplification measures // -- diff --git a/proxy/upstreams.go b/proxy/upstreams.go index 0bb2782d0..cf0f7f2b2 100644 --- a/proxy/upstreams.go +++ b/proxy/upstreams.go @@ -57,9 +57,10 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (* dnsUpstream, err = upstream.AddressToUpstream( u, &upstream.Options{ - Bootstrap: options.Bootstrap, - Timeout: options.Timeout, - InsecureSkipVerify: options.InsecureSkipVerify, + Bootstrap: options.Bootstrap, + Timeout: options.Timeout, + InsecureSkipVerify: options.InsecureSkipVerify, + TLSClientCertificates: options.TLSClientCertificates, }) if err != nil { err = fmt.Errorf("cannot prepare the upstream %s (%s): %s", l, options.Bootstrap, err) @@ -86,6 +87,7 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (* } } } + return &UpstreamConfig{ Upstreams: upstreams, DomainReservedUpstreams: domainReservedUpstreams, diff --git a/upstream/bootstrap.go b/upstream/bootstrap.go index 662d59ca7..a67f0b294 100755 --- a/upstream/bootstrap.go +++ b/upstream/bootstrap.go @@ -73,7 +73,6 @@ func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper, } b.dialContext = b.createDialContext(resolverAddresses) b.resolvedConfig = b.createTLSConfig(host) - return b, nil } @@ -160,7 +159,6 @@ func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { } else { ctx = context.Background() } - addrs, err := LookupParallel(ctx, n.resolvers, host) if err != nil { return nil, nil, fmt.Errorf("lookup %s: %w", host, err) @@ -179,12 +177,12 @@ func (n *bootstrapper) get() (*tls.Config, dialHandler, error) { // couldn't find any suitable IP address return nil, nil, fmt.Errorf("couldn't find any suitable IP address for host %s", host) } - n.Lock() defer n.Unlock() n.dialContext = n.createDialContext(resolved) n.resolvedConfig = n.createTLSConfig(host) + return n.resolvedConfig, n.dialContext, nil } @@ -212,6 +210,21 @@ func (n *bootstrapper) createTLSConfig(host string) *tls.Config { tlsConfig.NextProtos = compatProtoDQ } + if n.options.TLSClientCertificates != nil { + log.Printf("Passing TLS configuration with client authentication") + tlsConfig.Certificates = []tls.Certificate{*n.options.TLSClientCertificates} + } + + // The supported application level protocols should be specified only + // for DNS-over-HTTPS and DNS-over-QUIC connections. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2681. + if n.URL.Scheme != "tls" { + tlsConfig.NextProtos = append([]string{ + "http/1.1", http2.NextProtoTLS, NextProtoDQ, + }, compatProtoDQ...) + } + return tlsConfig } diff --git a/upstream/parallel.go b/upstream/parallel.go index 4d8fd86f2..227b937e7 100644 --- a/upstream/parallel.go +++ b/upstream/parallel.go @@ -156,7 +156,6 @@ type lookupResult struct { // Return nil and error if count of errors equals count of resolvers func LookupParallel(ctx context.Context, resolvers []*Resolver, host string) ([]net.IPAddr, error) { size := len(resolvers) - if size == 0 { return nil, errors.Error("no resolvers specified") } diff --git a/upstream/upstream.go b/upstream/upstream.go index abb5df43e..c8cf06ee4 100644 --- a/upstream/upstream.go +++ b/upstream/upstream.go @@ -2,6 +2,7 @@ package upstream import ( + "crypto/tls" "crypto/x509" "fmt" "net" @@ -45,6 +46,8 @@ type Options struct { // VerifyDNSCryptCertificate is callback to which the DNSCrypt server certificate will be passed. // is called in dnsCrypt.exchangeDNSCrypt; if error != nil then Upstream.Exchange() will return it VerifyDNSCryptCertificate func(cert *dnscrypt.Cert) error + + TLSClientCertificates *tls.Certificate // TLS certificates when DoH/DoT/DoQ Client Authentication is used } // Parse "host:port" string and validate port number @@ -76,6 +79,7 @@ func AddressToUpstream(address string, options *Options) (Upstream, error) { } if strings.Contains(address, "://") { + upstreamURL, err := url.Parse(address) if err != nil { return nil, fmt.Errorf("failed to parse %s: %w", address, err)