diff --git a/README.md b/README.md index ca5bb7d..9ba8ba4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # RouteDNS - DNS stub resolver and router -RouteDNS acts as a stub resolver that offers flexible configuration options with a focus on providing privacy as well as resiliency. It supports several DNS protocols such as plain UDP and TCP, DNS-over-TLS and DNS-over-HTTPS as input and output. In addition it's possible to build complex configurations allowing routing of queries based on query name, type or source address. Upstream resolvers can be grouped in various ways to provide failover, load-balancing, or performance. +RouteDNS acts as a stub resolver and proxy that offers flexible configuration options with a focus on providing privacy as well as resiliency. It supports several DNS protocols such as plain UDP and TCP, DNS-over-TLS and DNS-over-HTTPS as input and output. In addition it's possible to build complex configurations allowing routing of queries based on query name, type or source address as well as blocklists and name translation. Upstream resolvers can be grouped in various ways to provide failover, load-balancing, or performance. Features: -- Support for DNS-over-TLS (DoT) +- Support for DNS-over-TLS (DoT), client and server - Support for DNS-over-HTTPS (DoH) - Custom CAs and mutual-TLS - Support for plain DNS, UDP and TCP for incoming and outgoing requests @@ -17,7 +17,6 @@ Features: TODO: -- DNS-over-TLS listeners - DNS-over-HTTP listeners - Dot and DoH listeners should support padding as per [RFC7830](https://tools.ietf.org/html/rfc7830) and [RFC8467](https://tools.ietf.org/html/rfc8467) - Introduce logging levels diff --git a/client-tls.go b/client-tls.go deleted file mode 100644 index bb57092..0000000 --- a/client-tls.go +++ /dev/null @@ -1,46 +0,0 @@ -package rdns - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "io/ioutil" -) - -type ClientTLSOptions struct { - // CAs to trust. Defaults to the system's CA store. - CAFile string - - // Key and Certificate mutual TLS. Only required if the server expects - // a client certificate. - ClientKeyFile string - ClientCrtFile string -} - -// Config returns a TLS config for a client based on the options. -func (opt ClientTLSOptions) Config() (*tls.Config, error) { - tlsConfig := &tls.Config{} - - // Add client key/cert if provided - if opt.ClientCrtFile != "" && opt.ClientKeyFile != "" { - certificate, err := tls.LoadX509KeyPair(opt.ClientCrtFile, opt.ClientKeyFile) - if err != nil { - return nil, fmt.Errorf("failed to load client certificate from %s", opt.ClientCrtFile) - } - tlsConfig.Certificates = []tls.Certificate{certificate} - } - - // Load custom CA set if provided - if opt.CAFile != "" { - certPool := x509.NewCertPool() - b, err := ioutil.ReadFile(opt.CAFile) - if err != nil { - return nil, err - } - if ok := certPool.AppendCertsFromPEM(b); !ok { - return nil, fmt.Errorf("no CA certficates found in %s", opt.CAFile) - } - tlsConfig.RootCAs = certPool - } - return tlsConfig, nil -} diff --git a/cmd/routedns/config.go b/cmd/routedns/config.go index 686e188..7672abc 100644 --- a/cmd/routedns/config.go +++ b/cmd/routedns/config.go @@ -16,9 +16,13 @@ type config struct { } type listener struct { - Address string - Protocol string - Resolver string + Address string + Protocol string + Resolver string + CA string + ServerKey string `toml:"server-key"` + ServerCrt string `toml:"server-crt"` + MutualTLS bool `toml:"mutual-tls"` } type resolver struct { diff --git a/cmd/routedns/example-config/mutual-tls-client.toml b/cmd/routedns/example-config/mutual-tls-client.toml new file mode 100644 index 0000000..a166b29 --- /dev/null +++ b/cmd/routedns/example-config/mutual-tls-client.toml @@ -0,0 +1,20 @@ +# This is the client-half of a fully secure DoT configuration where the +# server is private and expects the client to present a cert by a CA it +# trusts. +title = "RouteDNS configuration" + +[resolvers] + + [resolvers.myserver-dot] + address = ":853" + protocol = "dot" + ca = "/path/to/ca.crt" + client-crt = "/path/to/client.crt" + client-key = "/path/to/client.crt" + +[listeners] + + [listeners.local] + address = ":53" + protocol = "udp" + resolver = "myserver-dot" diff --git a/cmd/routedns/example-config/mutual-tls-server.toml b/cmd/routedns/example-config/mutual-tls-server.toml new file mode 100644 index 0000000..bebd5d5 --- /dev/null +++ b/cmd/routedns/example-config/mutual-tls-server.toml @@ -0,0 +1,22 @@ +# This is the server-side of a secure DNS proxy where the server expects the +# client to present a signed certificate it trusts (mutual-TLS). Any query +# received from the client this way, will then be forwarded to Cloudflare via +# DoT by the server. +title = "RouteDNS configuration" + +[resolvers] + + [resolvers.cloudflare-dot] + address = "1.1.1.1:853" + protocol = "dot" + +[listeners] + + [listeners.local-dot] + address = ":853" + protocol = "dot" + resolver = "cloudflare-dot" + server-crt = "/path/to/server.crt" + server-key = "/path/to/server.key" + ca = "/path/to/ca.crt" + mutual-tls = true diff --git a/cmd/routedns/main.go b/cmd/routedns/main.go index bd41239..35268c2 100644 --- a/cmd/routedns/main.go +++ b/cmd/routedns/main.go @@ -57,17 +57,19 @@ func start(args []string) error { for id, r := range config.Resolvers { switch r.Protocol { case "dot": - opt := rdns.DoTClientOptions{ - ClientTLSOptions: rdns.ClientTLSOptions{CAFile: r.CA, ClientCrtFile: r.ClientCrt, ClientKeyFile: r.ClientKey}, - } - resolvers[id], err = rdns.NewDoTClient(r.Address, opt) + tlsConfig, err := rdns.TLSClientConfig(r.CA, r.ClientCrt, r.ClientKey) if err != nil { - return fmt.Errorf("failed to parse resolver config for '%s' : %s", id, err) + return err } + resolvers[id] = rdns.NewDoTClient(r.Address, rdns.DoTClientOptions{TLSConfig: tlsConfig}) case "doh": + tlsConfig, err := rdns.TLSClientConfig(r.CA, r.ClientCrt, r.ClientKey) + if err != nil { + return err + } opt := rdns.DoHClientOptions{ - Method: r.DoH.Method, - ClientTLSOptions: rdns.ClientTLSOptions{CAFile: r.CA, ClientCrtFile: r.ClientCrt, ClientKeyFile: r.ClientKey}, + Method: r.DoH.Method, + TLSConfig: tlsConfig, } resolvers[id], err = rdns.NewDoHClient(r.Address, opt) if err != nil { @@ -157,6 +159,13 @@ func start(args []string) error { listeners = append(listeners, rdns.NewDNSListener(l.Address, "tcp", resolver)) case "udp": listeners = append(listeners, rdns.NewDNSListener(l.Address, "udp", resolver)) + case "dot": + tlsConfig, err := rdns.TLSServerConfig(l.CA, l.ServerCrt, l.ServerKey, l.MutualTLS) + if err != nil { + return err + } + ln := rdns.NewDoTListener(l.Address, rdns.DoTListenerOptions{TLSConfig: tlsConfig}, resolver) + listeners = append(listeners, ln) default: return fmt.Errorf("unsupported protocol '%s' for listener '%s'", l.Protocol, id) } diff --git a/dohclient.go b/dohclient.go index 6fd0357..a54d01a 100644 --- a/dohclient.go +++ b/dohclient.go @@ -2,6 +2,7 @@ package rdns import ( "bytes" + "crypto/tls" "encoding/base64" "fmt" "io/ioutil" @@ -15,10 +16,10 @@ import ( // DoHClientOptions contains options used by the DNS-over-HTTP resolver. type DoHClientOptions struct { - ClientTLSOptions - // Query method, either GET or POST. If empty, POST is used. Method string + + TLSConfig *tls.Config } // DoHClient is a DNS-over-HTTP resolver with support fot HTTP/2. @@ -33,10 +34,6 @@ var _ Resolver = &DoHClient{} // NewDoHClient instantiates a new DNS-over-HTTPS resolver. func NewDoHClient(endpoint string, opt DoHClientOptions) (*DoHClient, error) { - tlsConfig, err := opt.Config() - if err != nil { - return nil, err - } // Parse the URL template template, err := uritemplates.Parse(endpoint) if err != nil { @@ -46,7 +43,7 @@ func NewDoHClient(endpoint string, opt DoHClientOptions) (*DoHClient, error) { // HTTP transport for this client tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, - TLSClientConfig: tlsConfig, + TLSClientConfig: opt.TLSConfig, DisableCompression: true, ResponseHeaderTimeout: time.Second, IdleConnTimeout: 30 * time.Second, diff --git a/dotclient.go b/dotclient.go index 956eecc..a7bb221 100644 --- a/dotclient.go +++ b/dotclient.go @@ -1,6 +1,7 @@ package rdns import ( + "crypto/tls" "fmt" "github.com/miekg/dns" @@ -14,25 +15,21 @@ type DoTClient struct { // DoTClientOptions contains options used by the DNS-over-TLS resolver. type DoTClientOptions struct { - ClientTLSOptions + TLSConfig *tls.Config } var _ Resolver = &DoTClient{} // NewDoTClient instantiates a new DNS-over-TLS resolver. -func NewDoTClient(endpoint string, opt DoTClientOptions) (*DoTClient, error) { - tlsConfig, err := opt.Config() - if err != nil { - return nil, err - } +func NewDoTClient(endpoint string, opt DoTClientOptions) *DoTClient { client := &dns.Client{ Net: "tcp-tls", - TLSConfig: tlsConfig, + TLSConfig: opt.TLSConfig, } return &DoTClient{ endpoint: endpoint, pipeline: NewPipeline(endpoint, client), - }, nil + } } // Resolve a DNS query. diff --git a/dotclient_test.go b/dotclient_test.go index 1ade275..5b01b68 100644 --- a/dotclient_test.go +++ b/dotclient_test.go @@ -8,9 +8,7 @@ import ( ) func TestDoTClientSimple(t *testing.T) { - d, err := NewDoTClient("dns.google:853", DoTClientOptions{}) - require.NoError(t, err) - + d := NewDoTClient("dns.google:853", DoTClientOptions{}) q := new(dns.Msg) q.SetQuestion("cloudflare.com.", dns.TypeA) r, err := d.Resolve(q, ClientInfo{}) @@ -21,12 +19,11 @@ func TestDoTClientSimple(t *testing.T) { func TestDoTClientCA(t *testing.T) { // Create client cert options with a CA. TODO: Should read the cert dynamically // to avoid failure when this expires or changes. - opt := DoTClientOptions{} - opt.CAFile = "testdata/DigiCertECCSecureServerCA.pem" + tlsConfig, err := TLSClientConfig("testdata/DigiCertECCSecureServerCA.pem", "", "") + require.NoError(t, err) // DoT client with valid CA - d, err := NewDoTClient("1.1.1.1:853", opt) - require.NoError(t, err) + d := NewDoTClient("1.1.1.1:853", DoTClientOptions{TLSConfig: tlsConfig}) q := new(dns.Msg) q.SetQuestion("cloudflare.com.", dns.TypeA) r, err := d.Resolve(q, ClientInfo{}) @@ -34,8 +31,7 @@ func TestDoTClientCA(t *testing.T) { require.NotEmpty(t, r.Answer) // DoT client with invalid CA - d, err = NewDoTClient("dns.google:853", opt) - require.NoError(t, err) + d = NewDoTClient("dns.google:853", DoTClientOptions{TLSConfig: tlsConfig}) q.SetQuestion("cloudflare.com.", dns.TypeA) _, err = d.Resolve(q, ClientInfo{}) require.Error(t, err) diff --git a/dotlistener.go b/dotlistener.go new file mode 100644 index 0000000..6a9d620 --- /dev/null +++ b/dotlistener.go @@ -0,0 +1,46 @@ +package rdns + +import ( + "crypto/tls" + "fmt" + + "github.com/miekg/dns" +) + +// DoTListener is a standard DNS listener for DNS-over-TLS. +type DoTListener struct { + *dns.Server +} + +var _ Listener = &DoTListener{} + +// DoTListenerOptions contains options used by the DNS-over-TLS server. +type DoTListenerOptions struct { + TLSConfig *tls.Config +} + +// NewDoTListener returns an instance of either a UDP or TCP DNS listener. +func NewDoTListener(addr string, opt DoTListenerOptions, resolver Resolver) *DoTListener { + return &DoTListener{ + Server: &dns.Server{ + Addr: addr, + Net: "tcp-tls", + TLSConfig: opt.TLSConfig, + Handler: listenHandler(resolver), + }, + } +} + +// Start the DNS listener. +func (s DoTListener) Start() error { + return s.ListenAndServe() +} + +// Stop the listener. +func (s DoTListener) Stop() error { + return s.Shutdown() +} + +func (s DoTListener) String() string { + return fmt.Sprintf("DoT(%s)", s.Addr) +} diff --git a/dotlistener_test.go b/dotlistener_test.go new file mode 100644 index 0000000..d74a280 --- /dev/null +++ b/dotlistener_test.go @@ -0,0 +1,88 @@ +package rdns + +import ( + "net" + "testing" + "time" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestDoTListenerSimple(t *testing.T) { + upstream := new(TestResolver) + + // Find a free port for the listener + addr, err := getLnAddress() + require.NoError(t, err) + + // Create the listener + tlsServerConfig, err := TLSServerConfig("", "testdata/server.crt", "testdata/server.key", false) + require.NoError(t, err) + + s := NewDoTListener(addr, DoTListenerOptions{TLSConfig: tlsServerConfig}, upstream) + go func() { + err := s.Start() + require.NoError(t, err) + }() + defer s.Stop() + time.Sleep(time.Second) + + // Make a client talking to the listener. Need to trust the issue of the server certificate. + tlsConfig, err := TLSClientConfig("testdata/ca.crt", "", "") + require.NoError(t, err) + c := NewDoTClient(addr, DoTClientOptions{TLSConfig: tlsConfig}) + + // Send a query to the client. This should be proxied through the listener and hit the test resolver. + q := new(dns.Msg) + q.SetQuestion("cloudflare.com.", dns.TypeA) + _, err = c.Resolve(q, ClientInfo{}) + require.NoError(t, err) + + // The upstream resolver should have seen the query + require.Equal(t, 1, upstream.HitCount()) +} + +func TestDoTListenerMutual(t *testing.T) { + upstream := new(TestResolver) + + // Find a free port for the listener + addr, err := getLnAddress() + require.NoError(t, err) + + // Create the listener, expecting client certs to be presented. + tlsServerConfig, err := TLSServerConfig("testdata/ca.crt", "testdata/server.crt", "testdata/server.key", true) + require.NoError(t, err) + s := NewDoTListener(addr, DoTListenerOptions{TLSConfig: tlsServerConfig}, upstream) + + go func() { + err := s.Start() + require.NoError(t, err) + }() + defer s.Stop() + time.Sleep(time.Second) + + // Make a client talking to the listener. Need to trust the issue of the server certificate and + // present a client certificate. + tlsClientConfig, err := TLSClientConfig("testdata/ca.crt", "testdata/client.crt", "testdata/client.key") + require.NoError(t, err) + c := NewDoTClient(addr, DoTClientOptions{TLSConfig: tlsClientConfig}) + + // Send a query to the client. This should be proxied through the listener and hit the test resolver. + q := new(dns.Msg) + q.SetQuestion("cloudflare.com.", dns.TypeA) + _, err = c.Resolve(q, ClientInfo{}) + require.NoError(t, err) + + // The upstream resolver should have seen the query + require.Equal(t, 1, upstream.HitCount()) +} + +func getLnAddress() (string, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", nil + } + defer l.Close() + return l.Addr().String(), nil +} diff --git a/example_test.go b/example_test.go index d803fc7..3f9b765 100644 --- a/example_test.go +++ b/example_test.go @@ -9,7 +9,7 @@ import ( func Example_resolver() { // Define resolver - r, _ := rdns.NewDoTClient("dns.google:853", rdns.DoTClientOptions{}) + r := rdns.NewDoTClient("dns.google:853", rdns.DoTClientOptions{}) // Build a query q := new(dns.Msg) diff --git a/testdata/ca.crt b/testdata/ca.crt new file mode 100644 index 0000000..ca46848 --- /dev/null +++ b/testdata/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhzCCA2+gAwIBAgIUCV1+Cu3x/nPkNerfzJZSSRm31K0wDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDEPMA0GA1UEAwwGVGVzdENBMB4XDTE5MDcx +MzE5NDM0MFoXDTI5MDcxMDE5NDM0MFowUzELMAkGA1UEBhMCWFgxFTATBgNVBAcM +DERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEPMA0G +A1UEAwwGVGVzdENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1k7K +sFMf8ZuCfy1F0ynvylVRXDWuzaajq8IyDa004w8i8UDGXyNOZqpGrvUHyIiXK8k2 +FkDeHImGD8IAYUv1bgc3n0Kw8h+gwFgS+0UONICksfoqW/8mOEBl8ywR7BDr1TGi +JZt/pWV+q0za9R5pa42AqpLNGId49R77EoWj4OSvzHNV3QuuuA6zULxYUuLfTdxV +D9OonduWbVZIDsAtP2x+zzh/JbZ+HdEEny2b0MPNiTYl+k+8tD2+BZYBxqb4mtwP +KjMXxma/Q12DTtNlDG5f20WkLup9H8bxGT4mQmAP04jibJUKHfeRHprkBACpQLNf +w7RkUOkMYVNqo4CIlCL2D6uVSzhVdKzo+aa+26MKq4MzEePINjvWc2KFCANoSlM0 +tjPzN+Whc8IhWb1f82r/SkJf1Jb8ZxP1bkk/fkica236Umr6jh/W3swal5moGlLU +5qWECGFDXefDwOgs/+5tp42kIpb5fdrjrjv56qxdqCI41cvMJEtEmlKimAr/V+iN +5RVk7d6VqD9Q/1ibbXAxIrsW85puzdnFF7kEHn8kthPPwmwIlRnr+M9G0UPJ/5pB +S7GK6BqEMvE9BVv46lOQW08TiPTg+c9Um5LX8QnglwtuZ4JUqopAKugtRmikMHhK +qTAj+QA2njMtcSzd2Qs/4jXsUcoch/NfYVjV3O0CAwEAAaNTMFEwHQYDVR0OBBYE +FDz8sQ7WgaRl++XS5TU5dZXPBOKGMB8GA1UdIwQYMBaAFDz8sQ7WgaRl++XS5TU5 +dZXPBOKGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAG+eUAYy +xVDZ9FWtBKw2b/cYPmFQBOchINatlS/2Bb8rtU45njDTHfPCTGWFj8IMVavpZtsc +YE+JrZgk4+ZXMjHuRd5iIRB5311YVPnsCCMdAvzemiocTKmPZysXJreSchGGchgd +kPvQuMRlJYtdG/KhiqqK4VUdHRhAG2i29ewG20toW/xThBTNXddu5AwBOwj4jSg4 +yEaw2n1DTQlPLSp/XUa16Il5ODAoljCNMJPiui+/cOIhXZ/lsTEHB0A8i8zfrawo +PF4/mkNWj3RTDtGuU5HgRGiIIj2k0LnMK36hMfq5xbV0u+Y95qnO8IngfyuOiBuK +XVO74BtDZOXZd0AZMI/Um/PiqyBmzjkmOPCrCtDvXwBxB7Uha6yq9IdylD+thD1f +w4dIVJnaYL7Q/6dOGXhtCB1fQ6pHwhdZWM/aNfgUNDTpb2tuLiI+c+cKacaUIYm0 +WU2ZshTESjt71imMOE/QlyzbRmSxsilw2yTVy7VvEIECpTjzb03ZPpMBZrIitS0f +PJu/6TXAGC0Wp1WaC9hY+iLldl5JgXrlYZDAh720s0K/o70JlzHlsJDvckZtw3NA +0SMNWfrS1hiWJNgaDYPExu9H/6wYAqLRmUTA8Gsqhq+bz0m7JlEgCWe+CezbdI+q +djQ/mnPQFW3izpxcDdei/VGy8kHprmN44tP7 +-----END CERTIFICATE----- diff --git a/testdata/ca.key b/testdata/ca.key new file mode 100644 index 0000000..a1b9483 --- /dev/null +++ b/testdata/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/testdata/client.crt b/testdata/client.crt new file mode 100644 index 0000000..f42574a --- /dev/null +++ b/testdata/client.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEKTCCAhGgAwIBAgIIO7CYEYT8kt8wDQYJKoZIhvcNAQELBQAwUzELMAkGA1UE +BhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBD +b21wYW55IEx0ZDEPMA0GA1UEAwwGVGVzdENBMB4XDTE5MDcxMzIwNTQ1MloXDTI5 +MDcxMzIwNTQ1MlowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA7MsOuLxHtXpgptWx8QBJRW6cVHT1eJPAR1Ewugej +cn21z7LX07lfPqjVILBdD/AjLE3IroS7Egbh7ta7ww1DcL8zUpAsIySTk3W0yc3v +4h4zXG+1x/ltTlCpbxE1sdgQYNaX2KrWtQVr8A1HY8GedkKwLFE9iL4T24jCkdKR +NvLjr573CYGtSkyNdNmK+bcz+JSW1yBT1+Ow964zfYMxte2/aYxpku/P8mTUASiY +Yf64n+kVMPF4iocpSZhaQQqNPZX9vNQgOW6XB2e43lf1/dKfxfLAmQ/2wa+tJF56 +fONg0ti6qZDHAfymRgGc8mUrDzWmmjzFrBqLLJPMvetQPwIDAQABo0AwPjAOBgNV +HQ8BAf8EBAMCAqQwHwYDVR0jBBgwFoAUPPyxDtaBpGX75dLlNTl1lc8E4oYwCwYD +VR0RBAQwAoIAMA0GCSqGSIb3DQEBCwUAA4ICAQBSCnhGxcaEYIvvzgzgxSUEXtM4 +GFwy20UEtVScVdhsTBMAr/hwV+VzUQ8kyILECcZKh0J7jPWevn33OTp+RbNOQFHX +VtGdSK5xHKgvkD/D2L5ZNgCcujy9855HS+xJWe7gKH8NyM8mK9eN/apUWTBhD0KZ +MxCJAFRjaKk29T5SZ+k9kanVvzvoF1WtsTAitwgKQxpOToVmQdzfkmXOAfPscP4H ++1mSCqi82duAX5fqH5oHjpw6hCaQmnamhOlRy85zkRJQzElSi+1rmLcVq0WvHdnN +ElqfHwHEEDeJAu/emGN4klrqNaJ/RdKUTMKX2M39Qej7DNHEzvgUFp02BUNuJ1pJ +IRCLDDF7ZB9u8lDho6cuFx2Q37hn7Lh1nnZrOnBQ0NttmAszgQZ4QLXddgPiPFjD +SlRn0QrRSQ4Xm9eLUT6WuWaVOvNLy4QF55lz2DHTRUA71ZNzyA+SNTRfIV3kFhiw +Mi0pmTKPNxXYQCFNn13K7Z+MU0ZDmUuAxHiRMiJWC2dP+sL3Ys+AoFhpZIZGCwtG +K96RZLvytSh+9wjUf40tyiJo+wUNRBbJCegfJR3kqVKu2WMQqMsIdv2jVyfzaMxJ +b9GtrFN2/47eBxOvv4nc14eKHHaukEOqcnEt9bH04r/skByb7+Tg5Wxb6Dea2ZAp +MNaP+DuJeNiGSZ/RwQ== +-----END CERTIFICATE----- + diff --git a/testdata/client.key b/testdata/client.key new file mode 100644 index 0000000..6a94a8d --- /dev/null +++ b/testdata/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA7MsOuLxHtXpgptWx8QBJRW6cVHT1eJPAR1Ewugejcn21z7LX +07lfPqjVILBdD/AjLE3IroS7Egbh7ta7ww1DcL8zUpAsIySTk3W0yc3v4h4zXG+1 +x/ltTlCpbxE1sdgQYNaX2KrWtQVr8A1HY8GedkKwLFE9iL4T24jCkdKRNvLjr573 +CYGtSkyNdNmK+bcz+JSW1yBT1+Ow964zfYMxte2/aYxpku/P8mTUASiYYf64n+kV +MPF4iocpSZhaQQqNPZX9vNQgOW6XB2e43lf1/dKfxfLAmQ/2wa+tJF56fONg0ti6 +qZDHAfymRgGc8mUrDzWmmjzFrBqLLJPMvetQPwIDAQABAoIBADoq63Pf9HGVHVb+ +WZbNLjKbKFXq4T38dZhPTnv0UmdWkCn0UeZVvBrlWG2ORV4b7Ff+x5RvUJDuFeQY +5cjVYkK5BT974QvE+WeY249TQmE2cAF+A5abJAm/8M8y2BDW9bcEIAr+dH3FVi2C +5Y2Qy39bhHFuh4ImxzP/lWRC6J8pdF+bcTsktmYSepf/CVH1SFvidBsnYLz9AQ/9 +8aD0nUJuSohphsjYm7VjKw6QkXhfgGrU947irHWzGa/2A8XoZ42HZpqgQxYTkRjp +lsNtdvYMjt6E9MbsxypSGniZOoQqVoJcVb/eKrf/1TWEG3aoIICki2MLRHsYz+W5 +x4jRBfkCgYEA+pETo4xFEieQSGYo/9Ud6tU/SnXpQTZnPP8jr81sPlOnnT/VJzPD +wPdWbnJ2XSDZduCv/S72x4qXXZUFWmhdX3esoU0Mb8pHd8JC+XIIeOU2KzEK88ds +ZdDiNZgjHTfp41VZ8XFrSrACLtLPAE9QE4HBgEtfKbiMk775HKuBBVUCgYEA8e2F +w10Vp4K0HhdEN3wUcEj9vocZ2OsE74rV9SnQBrAOTbC8R/KCmibRxdBoWh7Ujzru +cgdyG2Q4IEDUxc6HRycyxtV5E4MA6GbkSavhqnywCBVw4hTMan0tgAcwGqrPkBWA +T5GcNq83djxlTnfuk2XftSikyM3UzusOo197P0MCgYEA1GgPgeDy3IT1ZqpTryv2 +hI/pazGxXjrEIY0Xr3wwelVoDYGDLAxRsU760f3uINwr/P5TsgkR4e22ivo843r9 +TGSizsoF9O5Az2C6bcMhM3r7BHo6kpVHarg8SrqBac9wUeqUqHxBO7sg3piOKfES +LLceVaePMErlwIlvm8I3SVECgYBX3yoCt8Cxwyug8lp4vLy/vANOPMwKmfKE/yyP +i8xfYXsQhO0eRNtjGk5/Rx9f/GrAS2toR4QOpuwr6uBdqJJCKd6rkcYUbDTcNOMe +Tyv8PKXDieYid+N3mlf4dKPoS7pwXx7nx7+xrRq7+1vgkHc7WnlS15xiw0BUl9QN +SxT8+QKBgBrIwExjbXlznGmJiN4/h7yI8TG0ZLjXLfhGi4EmBqlSCiQ16wPtclsN +3wn+n7hdbDfQwaeHtU3qsIzhzu7R21fRUC4ytDOw0jXENhn6Vf8Br/p9Ak33aA8v +kdx71akiHTrt5StD2SmKxGIcT9o+ml8tqksiUzjj35PBf5FXHY9A +-----END RSA PRIVATE KEY----- diff --git a/testdata/server.crt b/testdata/server.crt new file mode 100644 index 0000000..d0b464f --- /dev/null +++ b/testdata/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAiCgAwIBAgIISnFN6lamqvQwDQYJKoZIhvcNAQELBQAwUzELMAkGA1UE +BhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBD +b21wYW55IEx0ZDEPMA0GA1UEAwwGVGVzdENBMB4XDTE5MDcxMzIwNDk1OFoXDTI5 +MDcxMzIwNDk1OFowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA6adodkS68PUKxEbDzrMcQ0tTTvlzg9yIMsEIKK2u +ShbymNnBF7RK0G4H77vbQsqjrzGL4zXHFjo3HAZx43nO/4ujVD7NnHfC4aa2sKCX +QIQdEodSBmOc5o7VKdBR9CZ8KTkTt/IVpQsVHxxDvwT3s9yjnVzULIjUICbJxMKX +g97BaD0mSZ7CMsPi5xT29rWikMtkpD7qJm7Xvt3vD9buahzFjoIgs/cs9S4G2uS6 +d+gftjsDDkH5EByv5ekDubqT3ZVEoEL7ew1DaYyWZAuGdtdT8p85VSBZtpUd5EnX +j1tz943hneLBTh7s1MhBW5xEOKo1iVU9ELiM+Hg6jtQ2EwIDAQABo08wTTAOBgNV +HQ8BAf8EBAMCAqQwHwYDVR0jBBgwFoAUPPyxDtaBpGX75dLlNTl1lc8E4oYwGgYD +VR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQAfvMtF +ffQqnqt21S6QOv6ZBMjtLO118dB+98xyVmddhB1qcC21RNiPAC7H27eqItZJ7/CZ +ygUZkhvmt5LBB+9OuPc/Tm6VazvjqYXtql0Ru8WttAxF8ZbLhe3CX0bSI+zvzWC1 +CSjjlNM8pATgZ4qRAJkGtwDUejEeqS5RyjJaeKEo3CqbSasZIBSGqRuBafk9QP3E +IyPQDFPKNLirA5yXVCLFpFzmkuPD/Rc7H4PieXNJ+/UbvBChRw5+3sRuzB2JAoF+ +A3RrC//HzHpa2HFNypIR9MBkfCSNYaCG6VtZNJGHojWLttVG4Y5w6yVZwL5snRrv +A5g3U8A8B2+rLR9q3aU9weax04XNZ6xQL208yfTm/8a/qYPpSvgDnN4ucF3anZTF +F2OiKJAcRMo7J+Y4L8M2eIz8RwoBYYauAz/TXBum76iI+zEqiz3TdW3juL5y007i +ys5WBJdBJ8LZiTiZz6ZQFcrww84w18mhhdKZtUveEitHRCvJy8Ei1n4+F1v14muY +jSts7RrO5h/LU+QlivCFGicxLzf/WaAYryDtFJeGDj22JN5cME9kxYouulNedhAM +HAZdkyw+ZyKRykUVHCMlIVfFnbB4QNHgtATEpMs1OkMn435e/HZHSpd1xsP+aVQi +MbT0k4OQg1FVlQQlleO1wD1GqxDFk5V4Hw/JyA== +-----END CERTIFICATE----- + diff --git a/testdata/server.key b/testdata/server.key new file mode 100644 index 0000000..2bb3e88 --- /dev/null +++ b/testdata/server.key @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6adodkS68PUKxEbDzrMcQ0tTTvlzg9yIMsEIKK2uShbymNnB +F7RK0G4H77vbQsqjrzGL4zXHFjo3HAZx43nO/4ujVD7NnHfC4aa2sKCXQIQdEodS +BmOc5o7VKdBR9CZ8KTkTt/IVpQsVHxxDvwT3s9yjnVzULIjUICbJxMKXg97BaD0m +SZ7CMsPi5xT29rWikMtkpD7qJm7Xvt3vD9buahzFjoIgs/cs9S4G2uS6d+gftjsD +DkH5EByv5ekDubqT3ZVEoEL7ew1DaYyWZAuGdtdT8p85VSBZtpUd5EnXj1tz943h +neLBTh7s1MhBW5xEOKo1iVU9ELiM+Hg6jtQ2EwIDAQABAoIBADasNHZQGMofHHjW +8iRgpsFcU88L8aquJLRzlJwoH7s4aWW4tkT8q/Dffj3rYB/d2LU6y7fLsp0R3ClT +nLyUmQoUu5AP7f6py1EPuHnV6e5vu3nFbj0Oe/06+MgC9dpCUxU2RNXq8IOg9z1D +WNrOp8NPYn2E5iTRk4k+akH/IoL5XxQOaypvlzXPoQq5iNMaYZjh3DCDhBTdPY0e +ySCMDVmzNtWpVflMx7eY08saKKYR8k4iEv1L/s6EB66ujfzB8fhKTb5mET8uXu0r +W3Wc4AacwYdhNB+x8htfYF0MKVoAQX6cpq8G/8aCyUunudi4JgI8v+PVgs/vu+ha +zGcmJJECgYEA/Rx0QBiPZo8HbHP189HeZx64ubAuxtBy0ktoS5blhX2CqZEI+kA3 +Hwl1YTon5kCZgE9n1+O88ScRJ7KFj6mxhAxQSkOST6xkrB4DDzDhvk9BYdT0Y7N/ +U3jF81vrJENyI4gaEsMzo4u+jTtyQL6EW7y03B7BrUf6JGbmPPcH2wsCgYEA7FIa +em9y7QlFUjwfPKB4zNgyvEZ8RVF0fmiWEhBFB+/qJbmGx0lipu4h/sp00N+avYQZ +eyPb66wqVDADKzji53sxAvbgp7aPHUmYk1Hw6XenK+QYQRMYifNUeIkyI0rzHzUa +4AERh5s8mg2syQhNIfRkS133Q152MYtU6XHCthkCgYEA9cL1FW9Dfd+O471MqSuz ++QiZcKMjKCQp+QsC/7JKte0CO8b5opNLcjVq5bbkGuvKoA151OPqB3VZcOQkTzQD +iyWxqAooPHg1X/HcIpnh9zlZULbHfS9CiDgSbJNpo9JhNyLlviYPM9NyeAuqijby +qWh3a+vpPmlO9p17HOL3m7UCgYBDFofXzD3XFvOsBc9kWbYRiSrWEjiDQT2OCUjZ +Ne8y8qQJM6MUfjvYYfAasgT5qxD1zkOhlqt/OzAFGGA96/dWeb2PNuDOG+CDEvqS +kAeRb9twdV+BUdd8iiynz2MBa+ybJmtLvmHommRY45rysz2abxPt5W4lnPJ23DQt +1ZLOOQKBgQCpt96v/5PT/tVgL3qvX2I8l8t+i8f+GcspnklB9BPtRfsSv4bwxv5b +BfxqHiJQo6/ZgtH+IszRpd1wKdl4WeZ0MGPjNeBw4QL82bZ7rmW9TQo4d1RNseBC +v7nMoj7hyHL13k3sgXoPf4tI7ZQoHd54pKJsy6fuuJ+qUZYuqhi20A== +-----END RSA PRIVATE KEY----- + diff --git a/tls.go b/tls.go new file mode 100644 index 0000000..0a7c4ca --- /dev/null +++ b/tls.go @@ -0,0 +1,67 @@ +package rdns + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" +) + +// TLSServerConfig is a convenience function that builds a tls.Config instance for TLS servers +// based on common options and certificate+key files. +func TLSServerConfig(caFile, crtFile, keyFile string, mutualTLS bool) (*tls.Config, error) { + tlsConfig := &tls.Config{} + if mutualTLS { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + if caFile != "" { + certPool := x509.NewCertPool() + b, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + if ok := certPool.AppendCertsFromPEM(b); !ok { + return nil, fmt.Errorf("no CA certficates found in %s", caFile) + } + tlsConfig.ClientCAs = certPool + } + + if crtFile != "" && keyFile != "" { + var err error + tlsConfig.Certificates = make([]tls.Certificate, 1) + tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + return nil, err + } + } + return tlsConfig, nil +} + +// TLSClientConfig is a convenience function that builds a tls.Config instance for TLS clients +// based on common options and certificate+key files. +func TLSClientConfig(caFile, crtFile, keyFile string) (*tls.Config, error) { + tlsConfig := &tls.Config{} + + // Add client key/cert if provided + if crtFile != "" && keyFile != "" { + certificate, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate from %s", crtFile) + } + tlsConfig.Certificates = []tls.Certificate{certificate} + } + + // Load custom CA set if provided + if caFile != "" { + certPool := x509.NewCertPool() + b, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + if ok := certPool.AppendCertsFromPEM(b); !ok { + return nil, fmt.Errorf("no CA certficates found in %s", caFile) + } + tlsConfig.RootCAs = certPool + } + return tlsConfig, nil +}