From 89eff28fac05e2bbaad199cb07c27ea358fa8bec Mon Sep 17 00:00:00 2001 From: Rob Best Date: Mon, 22 Jun 2020 16:50:21 +0100 Subject: [PATCH] Add starttls for smtp, imap and ftp (#36) --- README.md | 12 ++- config/config.go | 5 ++ examples/ssl_exporter.yaml | 4 + go.sum | 3 + prober/tcp.go | 149 ++++++++++++++++++++++++++++-- prober/tcp_test.go | 78 ++++++++++++++++ ssl_exporter.go | 1 + ssl_exporter_test.go | 180 ++++++++++++++++++++++++++++++++----- test/tcp.go | 99 +++++++++++++++++++- 9 files changed, 501 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 670550d..60b25c1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ meaningful visualisations and consoles. - [Configuration file](#configuration-file) - [<module>](#module) - [<tls_config>](#tls_config) + - [<https_probe>](#https_probe) + - [<tcp_probe>](#tcp_probe) - [Example Queries](#example-queries) - [Proxying](#proxying) - [Grafana](#grafana) @@ -152,7 +154,7 @@ modules: [] #### \ ``` -# The protocol over which the probe will take place (http, tcp) +# The protocol over which the probe will take place (https, tcp) prober: # Configuration for TLS @@ -160,6 +162,7 @@ prober: # The specific probe configuration [ https: ] +[ tcp: ] ``` #### @@ -188,6 +191,13 @@ prober: [ proxy_url: ] ``` +#### + +``` +# Use the STARTTLS command before starting TLS for those protocols that support it (smtp, ftp, imap) +[ starttls: ] +``` + ## Example Queries Certificates that expire within 7 days: diff --git a/config/config.go b/config/config.go index 6dc1fad..94faa7e 100644 --- a/config/config.go +++ b/config/config.go @@ -52,6 +52,11 @@ type Module struct { Prober string `yaml:"prober,omitempty"` TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"` HTTPS HTTPSProbe `yaml:"https,omitempty"` + TCP TCPProbe `yaml:"tcp,omitempty"` +} + +type TCPProbe struct { + StartTLS string `yaml:"starttls,omitempty"` } type HTTPSProbe struct { diff --git a/examples/ssl_exporter.yaml b/examples/ssl_exporter.yaml index 2e1d276..323247c 100644 --- a/examples/ssl_exporter.yaml +++ b/examples/ssl_exporter.yaml @@ -21,3 +21,7 @@ modules: ca_file: /etc/tls/ca.crt cert_file: /etc/tls/tls.crt key_file: /etc/tls/tls.key + tcp_smtp_starttls: + prober: tcp + tcp: + starttls: smtp diff --git a/go.sum b/go.sum index 37b92ac..c24e00b 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -69,6 +70,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -167,6 +169,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/prober/tcp.go b/prober/tcp.go index c4e1041..ff29fc9 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -1,29 +1,166 @@ package prober import ( + "bufio" "crypto/tls" + "fmt" "net" + "regexp" "time" "github.com/ribbybibby/ssl_exporter/config" pconfig "github.com/prometheus/common/config" + "github.com/prometheus/common/log" ) // ProbeTCP performs a tcp probe func ProbeTCP(target string, module config.Module, timeout time.Duration) (*tls.ConnectionState, error) { - tlsConfig, err := pconfig.NewTLSConfig(&module.TLSConfig) - if err != nil { - return nil, err - } + dialer := &net.Dialer{Timeout: timeout} - conn, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", target, tlsConfig) + conn, err := dialer.Dial("tcp", target) if err != nil { return nil, err } defer conn.Close() - state := conn.ConnectionState() + if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { + return nil, fmt.Errorf("Error setting deadline") + } + + if module.TCP.StartTLS != "" { + err = startTLS(conn, module.TCP.StartTLS) + if err != nil { + return nil, err + } + } + + tlsConfig, err := pconfig.NewTLSConfig(&module.TLSConfig) + if err != nil { + return nil, err + } + + if tlsConfig.ServerName == "" { + targetAddress, _, err := net.SplitHostPort(target) + if err != nil { + return nil, err + } + tlsConfig.ServerName = targetAddress + } + + tlsConn := tls.Client(conn, tlsConfig) + defer tlsConn.Close() + + if err := tlsConn.Handshake(); err != nil { + return nil, err + } + + state := tlsConn.ConnectionState() return &state, nil } + +type queryResponse struct { + expect string + send string +} + +var ( + // These are the protocols for which I had servers readily available to test + // against. There are plenty of other protocols that should be added here in + // the future. + // + // See openssl s_client for more examples: + // https://github.com/openssl/openssl/blob/openssl-3.0.0-alpha3/apps/s_client.c#L2229-L2728 + startTLSqueryResponses = map[string][]queryResponse{ + "smtp": []queryResponse{ + queryResponse{ + expect: "^220", + }, + queryResponse{ + send: "EHLO prober", + }, + queryResponse{ + expect: "^250-STARTTLS", + }, + queryResponse{ + send: "STARTTLS", + }, + queryResponse{ + expect: "^220", + }, + }, + "ftp": []queryResponse{ + queryResponse{ + expect: "^220", + }, + queryResponse{ + send: "AUTH TLS", + }, + queryResponse{ + expect: "^234", + }, + }, + "imap": []queryResponse{ + queryResponse{ + expect: "OK", + }, + queryResponse{ + send: ". CAPABILITY", + }, + queryResponse{ + expect: "STARTTLS", + }, + queryResponse{ + expect: "OK", + }, + queryResponse{ + send: ". STARTTLS", + }, + queryResponse{ + expect: "OK", + }, + }, + } +) + +// startTLS will send the STARTTLS command for the given protocol +func startTLS(conn net.Conn, proto string) error { + var err error + + qr, ok := startTLSqueryResponses[proto] + if !ok { + return fmt.Errorf("STARTTLS is not supported for %s", proto) + } + + scanner := bufio.NewScanner(conn) + for _, qr := range qr { + if qr.expect != "" { + var match bool + for scanner.Scan() { + log.Debugf("read line: %s", scanner.Text()) + match, err = regexp.Match(qr.expect, scanner.Bytes()) + if err != nil { + return err + } + if match { + log.Debugf("regex: %s matched: %s", qr.expect, scanner.Text()) + break + } + } + if scanner.Err() != nil { + return scanner.Err() + } + if !match { + return fmt.Errorf("regex: %s didn't match: %s", qr.expect, scanner.Text()) + } + } + if qr.send != "" { + log.Debugf("sending line: %s", qr.send) + if _, err := fmt.Fprintf(conn, "%s\r\n", qr.send); err != nil { + return err + } + } + } + return nil +} diff --git a/prober/tcp_test.go b/prober/tcp_test.go index 66de0e0..a37337b 100644 --- a/prober/tcp_test.go +++ b/prober/tcp_test.go @@ -154,3 +154,81 @@ func TestProbeTCPExpiredInsecure(t *testing.T) { t.Fatalf("expected state but got nil") } } + +// TestProbeTCPStartTLSSMTP tests STARTTLS against a mock SMTP server +func TestProbeTCPStartTLSSMTP(t *testing.T) { + server, _, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartSMTP() + defer server.Close() + + module := config.Module{ + TCP: config.TCPProbe{ + StartTLS: "smtp", + }, + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + InsecureSkipVerify: false, + }, + } + + if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + t.Fatalf("error: %s", err) + } +} + +// TestProbeTCPStartTLSFTP tests STARTTLS against a mock FTP server +func TestProbeTCPStartTLSFTP(t *testing.T) { + server, _, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartFTP() + defer server.Close() + + module := config.Module{ + TCP: config.TCPProbe{ + StartTLS: "ftp", + }, + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + InsecureSkipVerify: false, + }, + } + + if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + t.Fatalf("error: %s", err) + } +} + +// TestProbeTCPStartTLSIMAP tests STARTTLS against a mock IMAP server +func TestProbeTCPStartTLSIMAP(t *testing.T) { + server, _, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartIMAP() + defer server.Close() + + module := config.Module{ + TCP: config.TCPProbe{ + StartTLS: "imap", + }, + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + InsecureSkipVerify: false, + }, + } + + if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + t.Fatalf("error: %s", err) + } +} diff --git a/ssl_exporter.go b/ssl_exporter.go index fc3a736..bd93cd6 100644 --- a/ssl_exporter.go +++ b/ssl_exporter.go @@ -75,6 +75,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { state, err := e.prober(e.target, e.module, e.timeout) if err != nil { + log.Errorln(err) ch <- prometheus.MustNewConstMetric( tlsConnectSuccess, prometheus.GaugeValue, 0, ) diff --git a/ssl_exporter_test.go b/ssl_exporter_test.go index cb3f985..1e639e3 100644 --- a/ssl_exporter_test.go +++ b/ssl_exporter_test.go @@ -57,19 +57,9 @@ func TestProbeHandlerHTTPS(t *testing.T) { } // Check notAfter and notBefore metrics - block, _ := pem.Decode(certPEM) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { + if err := checkDates(certPEM, rr.Body.String()); err != nil { t.Errorf(err.Error()) } - notAfter := strconv.FormatFloat(float64(cert.NotAfter.UnixNano()/1e9), 'g', -1, 64) - if ok := strings.Contains(rr.Body.String(), "ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notAfter); !ok { - t.Errorf("expected `ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notAfter + "`") - } - notBefore := strconv.FormatFloat(float64(cert.NotBefore.UnixNano()/1e9), 'g', -1, 64) - if ok := strings.Contains(rr.Body.String(), "ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notBefore); !ok { - t.Errorf("expected `ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notBefore + "`") - } // Check TLS version metric ok := strings.Contains(rr.Body.String(), "ssl_tls_version_info{version=\"TLS 1.3\"} 1") @@ -237,19 +227,9 @@ func TestProbeHandlerTCP(t *testing.T) { } // Check notAfter and notBefore metrics - block, _ := pem.Decode(certPEM) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { + if err := checkDates(certPEM, rr.Body.String()); err != nil { t.Errorf(err.Error()) } - notAfter := strconv.FormatFloat(float64(cert.NotAfter.UnixNano()/1e9), 'g', -1, 64) - if ok := strings.Contains(rr.Body.String(), "ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notAfter); !ok { - t.Errorf("expected `ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notAfter + "`") - } - notBefore := strconv.FormatFloat(float64(cert.NotBefore.UnixNano()/1e9), 'g', -1, 64) - if ok := strings.Contains(rr.Body.String(), "ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notBefore); !ok { - t.Errorf("expected `ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notBefore + "`") - } } // TestProbeHandlerTCPNoServer tests against a tcp server that doesn't exist @@ -490,6 +470,162 @@ func TestProbeHandlerProxy(t *testing.T) { } } +// TestProbeHandlerTCPStartTLSSMTP tests STARTTLS with a smtp server +func TestProbeHandlerTCPStartTLSSMTP(t *testing.T) { + server, certPEM, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartSMTP() + defer server.Close() + + conf := &config.Config{ + Modules: map[string]config.Module{ + "smtp": config.Module{ + Prober: "tcp", + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + }, + TCP: config.TCPProbe{ + StartTLS: "smtp", + }, + }, + }, + } + + rr, err := probe(server.Listener.Addr().String(), "smtp", conf) + if err != nil { + t.Fatalf(err.Error()) + } + + // Check success metric + if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { + t.Errorf("expected `ssl_tls_connect_success 1`") + } + + // Check probe metric + if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok { + t.Errorf("expected `ssl_prober{prober=\"tcp\"} 1`") + } + + // Check notAfter and notBefore metrics + if err := checkDates(certPEM, rr.Body.String()); err != nil { + t.Errorf(err.Error()) + } +} + +// TestProbeHandlerTCPStartTLSFTP tests STARTTLS with a ftp server +func TestProbeHandlerTCPStartTLSFTP(t *testing.T) { + server, certPEM, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartFTP() + defer server.Close() + + conf := &config.Config{ + Modules: map[string]config.Module{ + "ftp": config.Module{ + Prober: "tcp", + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + }, + TCP: config.TCPProbe{ + StartTLS: "ftp", + }, + }, + }, + } + + rr, err := probe(server.Listener.Addr().String(), "ftp", conf) + if err != nil { + t.Fatalf(err.Error()) + } + + // Check success metric + if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { + t.Errorf("expected `ssl_tls_connect_success 1`") + } + + // Check probe metric + if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok { + t.Errorf("expected `ssl_prober{prober=\"tcp\"} 1`") + } + + // Check notAfter and notBefore metrics + if err := checkDates(certPEM, rr.Body.String()); err != nil { + t.Errorf(err.Error()) + } +} + +// TestProbeHandlerTCPStartTLSIMAP tests STARTTLS with an imap server +func TestProbeHandlerTCPStartTLSIMAP(t *testing.T) { + server, certPEM, _, caFile, teardown, err := test.SetupTCPServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + server.StartIMAP() + defer server.Close() + + conf := &config.Config{ + Modules: map[string]config.Module{ + "imap": config.Module{ + Prober: "tcp", + TLSConfig: pconfig.TLSConfig{ + CAFile: caFile, + }, + TCP: config.TCPProbe{ + StartTLS: "imap", + }, + }, + }, + } + + rr, err := probe(server.Listener.Addr().String(), "imap", conf) + if err != nil { + t.Fatalf(err.Error()) + } + + // Check success metric + if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { + t.Errorf("expected `ssl_tls_connect_success 1`") + } + + // Check probe metric + if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok { + t.Errorf("expected `ssl_prober{prober=\"tcp\"} 1`") + } + + // Check notAfter and notBefore metrics + if err := checkDates(certPEM, rr.Body.String()); err != nil { + t.Errorf(err.Error()) + } +} + +func checkDates(certPEM []byte, body string) error { + // Check notAfter and notBefore metrics + block, _ := pem.Decode(certPEM) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + notAfter := strconv.FormatFloat(float64(cert.NotAfter.UnixNano()/1e9), 'g', -1, 64) + if ok := strings.Contains(body, "ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notAfter); !ok { + return fmt.Errorf("expected `ssl_cert_not_after{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notAfter + "`") + } + notBefore := strconv.FormatFloat(float64(cert.NotBefore.UnixNano()/1e9), 'g', -1, 64) + if ok := strings.Contains(body, "ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} "+notBefore); !ok { + return fmt.Errorf("expected `ssl_cert_not_before{cn=\"example.ribbybibby.me\",dnsnames=\",example.ribbybibby.me,example-2.ribbybibby.me,example-3.ribbybibby.me,\",emails=\",me@ribbybibby.me,example@ribbybibby.me,\",ips=\",127.0.0.1,::1,\",issuer_cn=\"example.ribbybibby.me\",ou=\",ribbybibbys org,\",serial_no=\"100\"} " + notBefore + "`") + } + return nil +} + func probe(target, module string, conf *config.Config) (*httptest.ResponseRecorder, error) { uri := "/probe?target=" + target if module != "" { diff --git a/test/tcp.go b/test/tcp.go index 80b071a..b88a9b0 100644 --- a/test/tcp.go +++ b/test/tcp.go @@ -17,7 +17,7 @@ type TCPServer struct { stopCh chan struct{} } -// StartTLS starts a listener that performs a TLS handshake +// StartTLS starts a listener that performs an immediate TLS handshake func (t *TCPServer) StartTLS() { go func() { ln := tls.NewListener(t.Listener, t.TLS) @@ -39,6 +39,103 @@ func (t *TCPServer) StartTLS() { }() } +// StartSMTP starts a listener that negotiates a TLS connection with an smtp +// client using STARTTLS +func (t *TCPServer) StartSMTP() { + go func() { + conn, err := t.Listener.Accept() + if err != nil { + panic(fmt.Sprintf("Error accepting on socket: %s", err)) + } + defer conn.Close() + + if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + panic("Error setting deadline") + } + + fmt.Fprintf(conn, "220 ESMTP StartTLS pseudo-server\n") + if _, e := fmt.Fscanf(conn, "EHLO prober\n"); e != nil { + panic("Error in dialog. No EHLO received.") + } + fmt.Fprintf(conn, "250-pseudo-server.example.net\n") + fmt.Fprintf(conn, "250-STARTTLS\n") + fmt.Fprintf(conn, "250 DSN\n") + + if _, e := fmt.Fscanf(conn, "STARTTLS\n"); e != nil { + panic("Error in dialog. No (TLS) STARTTLS received.") + } + fmt.Fprintf(conn, "220 2.0.0 Ready to start TLS\n") + + // Upgrade to TLS. + tlsConn := tls.Server(conn, t.TLS) + if err := tlsConn.Handshake(); err != nil { + log.Errorln(err) + } + defer tlsConn.Close() + + t.stopCh <- struct{}{} + }() +} + +// StartFTP starts a listener that negotiates a TLS connection with an ftp +// client using AUTH TLS +func (t *TCPServer) StartFTP() { + go func() { + conn, err := t.Listener.Accept() + if err != nil { + panic(fmt.Sprintf("Error accepting on socket: %s", err)) + } + defer conn.Close() + + fmt.Fprintf(conn, "220 Test FTP Service\n") + if _, e := fmt.Fscanf(conn, "AUTH TLS\n"); e != nil { + panic("Error in dialog. No AUTH TLS received.") + } + fmt.Fprintf(conn, "234 AUTH command ok. Expecting TLS Negotiation.\n") + + // Upgrade to TLS. + tlsConn := tls.Server(conn, t.TLS) + if err := tlsConn.Handshake(); err != nil { + log.Errorln(err) + } + defer tlsConn.Close() + + t.stopCh <- struct{}{} + }() +} + +// StartIMAP starts a listener that negotiates a TLS connection with an imap +// client using STARTTLS +func (t *TCPServer) StartIMAP() { + go func() { + conn, err := t.Listener.Accept() + if err != nil { + panic(fmt.Sprintf("Error accepting on socket: %s", err)) + } + defer conn.Close() + + fmt.Fprintf(conn, "* OK XIMAP ready for requests\n") + if _, e := fmt.Fscanf(conn, ". CAPABILITY\n"); e != nil { + panic("Error in dialog. No . CAPABILITY received.") + } + fmt.Fprintf(conn, "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN STARTTLS\n") + fmt.Fprintf(conn, ". OK CAPABILITY completed.\n") + if _, e := fmt.Fscanf(conn, ". STARTTLS\n"); e != nil { + panic("Error in dialog. No . STARTTLS received.") + } + fmt.Fprintf(conn, ". OK Begin TLS negotiation now.\n") + + // Upgrade to TLS. + tlsConn := tls.Server(conn, t.TLS) + if err := tlsConn.Handshake(); err != nil { + log.Errorln(err) + } + defer tlsConn.Close() + + t.stopCh <- struct{}{} + }() +} + // Close stops the server and closes the listener func (t *TCPServer) Close() { <-t.stopCh