Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS Client authentication #172

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ dnsproxy
dnsproxy.exe
example.crt
example.key
coverage.txt
coverage.txt
client.crt
client.key
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
A simple DNS proxy server that supports all existing DNS protocols including `DNS-over-TLS`, `DNS-over-HTTPS`, `DNSCrypt`, and `DNS-over-QUIC`. Moreover, it can work as a `DNS-over-HTTPS`, `DNS-over-TLS` or `DNS-over-QUIC` server.

> Note that `DNS-over-QUIC` support is experimental, don't use it in production.
> Note that `DoH/DoT/DoQ` client authentication support is experimental, only DoH authentication has been tested
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? We can test this rather easily with a unit-test for all three


- [How to build](#how-to-build)
- [Usage](#usage)
Expand Down Expand Up @@ -77,6 +78,8 @@ Application Options:
--udp-buf-size= Set the size of the UDP buffer in bytes. A value <= 0 will use the system default. (default: 0)
--max-go-routines= Set the maximum number of go routines. A value <= 0 will not not set a maximum. (default: 0)
--version Prints the program version
--a-tls-crt= Path to the file to the tls certificate used to DoH/DoT/DoQ Client when client-authentication is enabled
--a-tls-key= Path to the file to the tls key used to DoH/DoT/DoQ Client when client-authentication is enabled

Help Options:
-h, --help Show this help message
Expand Down Expand Up @@ -118,6 +121,11 @@ DNS-over-HTTPS upstream with specified bootstrap DNS:
./dnsproxy -u https://dns.adguard.com/dns-query -b 1.1.1.1:53
```

DNS-over-HTTPS upstream with specified bootstrap DNS and Client authentication:
```shell
./dnsproxy -l 127.0.0.1 -u https://dns.plido.net/dns-query --a-tls-crt=/home/.../dohclient.cert.pem --a-tls-key=/home/.../dohclient.key.pem -b 1.1.1.1:53
```

DNS-over-QUIC upstream:
```shell
./dnsproxy -u quic://dns.adguard.com
Expand Down
41 changes: 38 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ type Options struct {
// Upstream DNS servers settings
// --

// DoH Upstream Authentication

// Path to the .crt with the clien-side certificate for upstream client authentication
TLSAuthCertPath string `long:"a-tls-crt" description:"Path to a file with the client certificate"`

// Path to the file with the clien-side private key for upstream client authentication
TLSAuthKeyPath string `long:"a-tls-key" description:"Path to a file with the client private key"`

// DNS upstreams
Upstreams []string `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" required:"true"`

Expand Down Expand Up @@ -160,6 +168,8 @@ var VersionString = "undefined" // nolint:gochecknoglobals

const defaultTimeout = 10 * time.Second

var tlsclient = false

// defaultDNS64Prefix is a so-called "Well-Known Prefix" for DNS64.
// if dnsproxy operates as a DNS64 server, we'll be using it.
const defaultDNS64Prefix = "64:ff9b::/96"
Expand Down Expand Up @@ -249,6 +259,7 @@ func createProxyConfig(options *Options) proxy.Config {
MaxGoroutines: options.MaxGoRoutines,
}

initClientTLSConfig(&config, options)
initUpstreams(&config, options)
initEDNS(&config, options)
initBogusNXDomain(&config, options)
Expand All @@ -269,6 +280,8 @@ func initUpstreams(config *proxy.Config, options *Options) {
InsecureSkipVerify: options.Insecure,
Bootstrap: options.BootstrapDNS,
Timeout: defaultTimeout,
TLSClientConfig: config.TLSClientConfig,
TLSClient: tlsclient,
})
if err != nil {
log.Fatalf("error while parsing upstreams configuration: %s", err)
Expand Down Expand Up @@ -298,6 +311,7 @@ func initUpstreams(config *proxy.Config, options *Options) {
}
config.Fallbacks = fallbacks
}

}

// initEDNS inits EDNS-related config
Expand Down Expand Up @@ -342,6 +356,18 @@ func initTLSConfig(config *proxy.Config, options *Options) {
}
}

// initTLSConfig inits the DoH Client Auth TLS config
func initClientTLSConfig(config *proxy.Config, options *Options) {
if options.TLSAuthCertPath != "" && options.TLSAuthKeyPath != "" {
tlsConfig, err := newTLSConfig(options)
if err != nil {
log.Fatalf("failed to load Client-auth TLS config: %s", err)
}
tlsclient = true
config.TLSClientConfig = tlsConfig
}
}

// initDNSCryptConfig inits the DNSCrypt config
func initDNSCryptConfig(config *proxy.Config, options *Options) {
if options.DNSCryptConfigPath == "" {
Expand Down Expand Up @@ -492,9 +518,18 @@ func newTLSConfig(options *Options) (*tls.Config, error) {
tlsMaxVersion = tls.VersionTLS12
}

cert, err := loadX509KeyPair(options.TLSCertPath, options.TLSKeyPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert: %s", err)
cert, err := loadX509KeyPair("", "")

if options.TLSAuthCertPath != "" {
cert, err = loadX509KeyPair(options.TLSAuthCertPath, options.TLSAuthKeyPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert for TLS client authentication: %s", err)
}
} else {
cert, err = loadX509KeyPair(options.TLSCertPath, options.TLSKeyPath)
if err != nil {
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
Expand Down
1 change: 1 addition & 0 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Config struct {
// --

TLSConfig *tls.Config // necessary for TLS, HTTPS, QUIC
TLSClientConfig *tls.Config // necessary for DoH/DoT/DoQ Client Authentication
DNSCryptProviderName string // DNSCrypt provider name
DNSCryptResolverCert *dnscrypt.Cert // DNSCrypt resolver certificate

Expand Down
3 changes: 3 additions & 0 deletions proxy/upstreams.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
Bootstrap: options.Bootstrap,
Timeout: options.Timeout,
InsecureSkipVerify: options.InsecureSkipVerify,
TLSClientConfig: options.TLSClientConfig.Clone(), //TODO Verify i we need an if
TLSClient: options.TLSClient,
})
if err != nil {
err = fmt.Errorf("cannot prepare the upstream %s (%s): %s", l, options.Bootstrap, err)
Expand All @@ -86,6 +88,7 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
}
}
}

return &UpstreamConfig{
Upstreams: upstreams,
DomainReservedUpstreams: domainReservedUpstreams,
Expand Down
11 changes: 8 additions & 3 deletions upstream/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper,
}
b.dialContext = b.createDialContext(resolverAddresses)
b.resolvedConfig = b.createTLSConfig(host)

return b, nil
}

Expand Down Expand Up @@ -155,7 +154,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, errorx.Decorate(err, "failed to lookup %s", host)
Expand All @@ -174,17 +172,18 @@ 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
}

// createTLSConfig creates a client TLS config
func (n *bootstrapper) createTLSConfig(host string) *tls.Config {

tlsConfig := &tls.Config{
ServerName: host,
RootCAs: RootCAs,
Expand All @@ -194,6 +193,12 @@ func (n *bootstrapper) createTLSConfig(host string) *tls.Config {
VerifyPeerCertificate: n.options.VerifyServerCertificate,
}

if n.options.TLSClient {
log.Printf("Creating a new TLS configuration with client authentication")
tlsConfig = &tls.Config{
Certificates: n.options.TLSClientConfig.Clone().Certificates,
}
}
// The supported application level protocols should be specified only
// for DNS-over-HTTPS and DNS-over-QUIC connections.
//
Expand Down
2 changes: 1 addition & 1 deletion upstream/bootstrap_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type Resolver struct {
// options.
func NewResolver(resolverAddress string, options *Options) (*Resolver, error) {
r := &Resolver{}

// set default net.Resolver as a resolver if resolverAddress is empty
if resolverAddress == "" {
r.resolver = &net.Resolver{}
Expand Down Expand Up @@ -122,6 +121,7 @@ type resultError struct {
}

func (r *Resolver) resolve(host string, qtype uint16, ch chan *resultError) {

req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
Expand Down
1 change: 0 additions & 1 deletion upstream/parallel.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,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")
}
Expand Down
7 changes: 7 additions & 0 deletions upstream/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package upstream

import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
Expand Down Expand Up @@ -46,6 +47,10 @@ 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

TLSClientConfig *tls.Config // TLS config when DoH/DoT/DoQ Client Authentication is used

TLSClient bool // TLS client authentication flag when DoH/DoT/DoQ Client Authentication is used
}

// Parse "host:port" string and validate port number
Expand All @@ -72,11 +77,13 @@ func parseHostAndPort(addr string) (string, string, error) {
// * sdns://... -- DNS stamp (see https://dnscrypt.info/stamps-specifications)
// options -- Upstream customization options, nil means default options.
func AddressToUpstream(address string, options *Options) (Upstream, error) {

if options == nil {
options = &Options{}
}

if strings.Contains(address, "://") {

upstreamURL, err := url.Parse(address)
if err != nil {
return nil, errorx.Decorate(err, "failed to parse %s", address)
Expand Down