diff --git a/.travis.yml b/.travis.yml index c93090c2..56a53c6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: -- 1.4 - 1.5 install: - DST=~/gopath/src/github.com/nats-io diff --git a/README.md b/README.md index 01ff682d..5fef0695 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gnatsd -[![License][License-Image]][License-Url] [![Build][Build-Status-Image]][Build-Status-Url] [![Release][Release-Image]][Release-Url] [![Coverage][Coverage-Image]][Coverage-Url] +[![License][License-Image]][License-Url] [![ReportCard][ReportCard-Image]][ReportCard-Url] [![Build][Build-Status-Image]][Build-Status-Url] [![Release][Release-Image]][Release-Url] [![Coverage][Coverage-Image]][Coverage-Url] A High Performance [NATS](https://nats.io) Server written in [Go.](http://golang.org) @@ -319,4 +319,6 @@ IN THE SOFTWARE. [Release-Url]: https://github.com/nats-io/gnatsd/releases/tag/v0.6.8 [Release-image]: http://img.shields.io/badge/release-v0.6.8-1eb0fc.svg [Coverage-Url]: https://coveralls.io/r/nats-io/gnatsd?branch=master -[Coverage-image]: https://img.shields.io/coveralls/nats-io/gnatsd.svg \ No newline at end of file +[Coverage-image]: https://img.shields.io/coveralls/nats-io/gnatsd.svg +[ReportCard-Url]: http://goreportcard.com/report/nats-io/gnatsd +[ReportCard-Image]: http://goreportcard.com/badge/nats-io/gnatsd diff --git a/gnatsd.go b/gnatsd.go index 862f81d7..9c45d2fd 100644 --- a/gnatsd.go +++ b/gnatsd.go @@ -55,6 +55,12 @@ func main() { flag.StringVar(&opts.RoutesStr, "routes", "", "Routes to actively solicit a connection.") flag.BoolVar(&showTlsHelp, "help_tls", false, "TLS help.") + flag.BoolVar(&opts.TLS, "tls", false, "Enable TLS.") + flag.BoolVar(&opts.TLSVerify, "tlsverify", false, "Enable TLS with client verification.") + flag.StringVar(&opts.TLSCert, "tlscert", "", "Server certificate file.") + flag.StringVar(&opts.TLSKey, "tlskey", "", "Private key for server certificate.") + flag.StringVar(&opts.TLSCaCert, "tlscacert", "", "Client certificate CA for verification.") + // Not public per se, will be replaced with dynamic system, but can be used to lower memory footprint when // lots of connections present. flag.IntVar(&opts.BufSize, "bs", 0, "Read/Write buffer size per client connection.") @@ -104,6 +110,9 @@ func main() { } opts.Routes = newroutes + // Configure TLS based on any present flags + configureTLS(&opts) + // Create the server with appropriate options. s := server.New(&opts) @@ -154,3 +163,29 @@ func configureLogger(s *server.Server, opts *server.Options) { s.SetLogger(log, opts.Debug, opts.Trace) } + +func configureTLS(opts *server.Options) { + // If no trigger flags, ignore the others + if !opts.TLS && !opts.TLSVerify { + return + } + if opts.TLSCert == "" { + server.PrintAndDie("TLS Server certificate must be present and valid.") + } + if opts.TLSKey == "" { + server.PrintAndDie("TLS Server private key must be present and valid.") + } + + tc := server.TLSConfigOpts{} + tc.CertFile = opts.TLSCert + tc.KeyFile = opts.TLSKey + tc.CaFile = opts.TLSCaCert + + if opts.TLSVerify { + tc.Verify = true + } + var err error + if opts.TLSConfig, err = server.GenTLSConfig(&tc); err != nil { + server.PrintAndDie(err.Error()) + } +} diff --git a/server/ciphersuites_1.4.go b/server/ciphersuites_1.4.go new file mode 100644 index 00000000..731cf2e7 --- /dev/null +++ b/server/ciphersuites_1.4.go @@ -0,0 +1,33 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +// +build go1.4,!go1.5 + +package server + +import ( + "crypto/tls" +) + +// Where we maintain all of the available 1.4 ciphers +var cipherMap = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, +} + +func defaultCipherSuites() []uint16 { + return []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } +} diff --git a/server/ciphersuites_1.5.go b/server/ciphersuites_1.5.go new file mode 100644 index 00000000..7b382b5b --- /dev/null +++ b/server/ciphersuites_1.5.go @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +// +build go1.5 + +package server + +import ( + "crypto/tls" +) + +// Where we maintain all of the available 1.5 ciphers +var cipherMap = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, +} + +func defaultCipherSuites() []uint16 { + return []uint16{ + // The SHA384 versions are only in Go1.5 + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } +} diff --git a/server/configs/tls.conf b/server/configs/tls.conf index 54fe8788..e647f4cf 100644 --- a/server/configs/tls.conf +++ b/server/configs/tls.conf @@ -7,6 +7,7 @@ net: apcera.me # net interface tls { cert_file: "./configs/certs/server.pem" key_file: "./configs/certs/key.pem" + timeout: 0.5 } authorization { diff --git a/server/configs/tls_ciphers.conf b/server/configs/tls_ciphers.conf index 3e24016b..d6a5676e 100644 --- a/server/configs/tls_ciphers.conf +++ b/server/configs/tls_ciphers.conf @@ -21,8 +21,6 @@ tls { "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" -# "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", -# "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" ] } diff --git a/server/const.go b/server/const.go index b0b6548f..b15e37fd 100644 --- a/server/const.go +++ b/server/const.go @@ -35,11 +35,11 @@ const ( // DEFAULT_MAX_CONNECTIONS is the default maximum connections allowed. DEFAULT_MAX_CONNECTIONS = (64 * 1024) - // SSL_TIMEOUT is the TLS/SSL wait time. - SSL_TIMEOUT = 500 * time.Millisecond + // TLS_TIMEOUT is the TLS wait time. + TLS_TIMEOUT = 500 * time.Millisecond // AUTH_TIMEOUT is the authorization wait time. - AUTH_TIMEOUT = 2 * SSL_TIMEOUT + AUTH_TIMEOUT = 2 * TLS_TIMEOUT // DEFAULT_PING_INTERVAL is how often pings are sent to clients and routes. DEFAULT_PING_INTERVAL = 2 * time.Minute diff --git a/server/opts.go b/server/opts.go index b74f401e..f6280def 100644 --- a/server/opts.go +++ b/server/opts.go @@ -42,6 +42,7 @@ type Options struct { ClusterUsername string `json:"-"` ClusterPassword string `json:"-"` ClusterAuthTimeout float64 `json:"auth_timeout"` + ClusterTLSTimeout float64 `json:"-"` ClusterTLSConfig *tls.Config `json:"-"` ProfPort int `json:"-"` PidFile string `json:"-"` @@ -52,6 +53,11 @@ type Options struct { RoutesStr string `json:"-"` BufSize int `json:"-"` TLSTimeout float64 `json:"tls_timeout"` + TLS bool `json:"-"` + TLSVerify bool `json:"-"` + TLSCert string `json:"-"` + TLSKey string `json:"-"` + TLSCaCert string `json:"-"` TLSConfig *tls.Config `json:"-"` } @@ -62,12 +68,14 @@ type authorization struct { } // This struct holds the parsed tls config information. -type tlsConfig struct { - certFile string - keyFile string - caFile string - verify bool - ciphers []uint16 +// It's public so we can use it for flag parsing +type TLSConfigOpts struct { + CertFile string + KeyFile string + CaFile string + Verify bool + Timeout float64 + Ciphers []uint16 } // ProcessConfigFile processes a configuration file. @@ -134,9 +142,14 @@ func ProcessConfigFile(configFile string) (*Options, error) { opts.MaxConn = int(v.(int64)) case "tls": tlsm := v.(map[string]interface{}) - if opts.TLSConfig, err = parseTLS(tlsm); err != nil { + tc, err := parseTLS(tlsm) + if err != nil { return nil, err } + if opts.TLSConfig, err = GenTLSConfig(tc); err != nil { + return nil, err + } + opts.TLSTimeout = tc.Timeout } } return opts, nil @@ -168,11 +181,15 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { opts.Routes = append(opts.Routes, url) } case "tls": - var err error tlsm := mv.(map[string]interface{}) - if opts.ClusterTLSConfig, err = parseTLS(tlsm); err != nil { + tc, err := parseTLS(tlsm) + if err != nil { return err } + if opts.ClusterTLSConfig, err = GenTLSConfig(tc); err != nil { + return err + } + opts.ClusterTLSTimeout = tc.Timeout } } return nil @@ -201,25 +218,6 @@ func parseAuthorization(am map[string]interface{}) authorization { return auth } -// keep one place where we hold all of the available ciphers -var cipherMap = map[string]uint16{ - "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - // go 1.5 "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - // go 1.5 "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, -} - // For Usage... func PrintTlsHelpAndDie() { @@ -263,8 +261,8 @@ func parseCipher(cipherName string) (uint16, error) { } // Helper function to parse TLS configs. -func parseTLS(tlsm map[string]interface{}) (*tls.Config, error) { - tc := tlsConfig{} +func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) { + tc := TLSConfigOpts{} for mk, mv := range tlsm { switch strings.ToLower(mk) { case "cert_file": @@ -272,53 +270,64 @@ func parseTLS(tlsm map[string]interface{}) (*tls.Config, error) { if !ok { return nil, fmt.Errorf("error parsing tls config, expected 'cert_file' to be filename") } - tc.certFile = certFile + tc.CertFile = certFile case "key_file": keyFile, ok := mv.(string) if !ok { return nil, fmt.Errorf("error parsing tls config, expected 'key_file' to be filename") } - tc.keyFile = keyFile + tc.KeyFile = keyFile case "ca_file": caFile, ok := mv.(string) if !ok { return nil, fmt.Errorf("error parsing tls config, expected 'ca_file' to be filename") } - tc.caFile = caFile + tc.CaFile = caFile case "verify": verify, ok := mv.(bool) if !ok { return nil, fmt.Errorf("error parsing tls config, expected 'verify' to be a boolean") } - tc.verify = verify + tc.Verify = verify case "cipher_suites": ra := mv.([]interface{}) if len(ra) == 0 { return nil, fmt.Errorf("error parsing tls config, 'cipher_suites' cannot be empty.") } - tc.ciphers = make([]uint16, 0, len(ra)) + tc.Ciphers = make([]uint16, 0, len(ra)) for _, r := range ra { cipher, err := parseCipher(r.(string)) if err != nil { return nil, err } - tc.ciphers = append(tc.ciphers, cipher) + tc.Ciphers = append(tc.Ciphers, cipher) } + case "timeout": + at := float64(0) + switch mv.(type) { + case int64: + at = float64(mv.(int64)) + case float64: + at = mv.(float64) + } + tc.Timeout = at default: return nil, fmt.Errorf("error parsing tls config, unknown field [%q]", mk) } } - // use some sane defaults - if tc.ciphers == nil { - tc.ciphers = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - } + // If cipher suites were not specified then use the defaults + if tc.Ciphers == nil { + tc.Ciphers = defaultCipherSuites() } + return &tc, nil +} + +func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { + // Now load in cert and private key - cert, err := tls.LoadX509KeyPair(tc.certFile, tc.keyFile) + cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile) if err != nil { return nil, fmt.Errorf("error parsing X509 certificate/key pair: %v", err) } @@ -333,16 +342,16 @@ func parseTLS(tlsm map[string]interface{}) (*tls.Config, error) { Certificates: []tls.Certificate{cert}, PreferServerCipherSuites: true, MinVersion: tls.VersionTLS12, - CipherSuites: tc.ciphers, + CipherSuites: tc.Ciphers, } // Require client certificates as needed - if tc.verify == true { + if tc.Verify == true { config.ClientAuth = tls.RequireAnyClientCert } // Add in CAs if applicable. - if tc.caFile != "" { - rootPEM, err := ioutil.ReadFile(tc.caFile) + if tc.CaFile != "" { + rootPEM, err := ioutil.ReadFile(tc.CaFile) if err != nil || rootPEM == nil { return nil, err } @@ -531,11 +540,14 @@ func processOptions(opts *Options) { opts.MaxPingsOut = DEFAULT_PING_MAX_OUT } if opts.TLSTimeout == 0 { - opts.TLSTimeout = float64(SSL_TIMEOUT) / float64(time.Second) + opts.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) } if opts.AuthTimeout == 0 { opts.AuthTimeout = float64(AUTH_TIMEOUT) / float64(time.Second) } + if opts.ClusterTLSTimeout == 0 { + opts.ClusterTLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } if opts.ClusterAuthTimeout == 0 { opts.ClusterAuthTimeout = float64(AUTH_TIMEOUT) / float64(time.Second) } diff --git a/server/opts_test.go b/server/opts_test.go index b77f4eee..aee1c2cc 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -17,12 +17,13 @@ func TestDefaultOptions(t *testing.T) { MaxConn: DEFAULT_MAX_CONNECTIONS, PingInterval: DEFAULT_PING_INTERVAL, MaxPingsOut: DEFAULT_PING_MAX_OUT, - TLSTimeout: float64(SSL_TIMEOUT) / float64(time.Second), + TLSTimeout: float64(TLS_TIMEOUT) / float64(time.Second), AuthTimeout: float64(AUTH_TIMEOUT) / float64(time.Second), MaxControlLine: MAX_CONTROL_LINE_SIZE, MaxPayload: MAX_PAYLOAD_SIZE, MaxPending: MAX_PENDING_SIZE, ClusterAuthTimeout: float64(AUTH_TIMEOUT) / float64(time.Second), + ClusterTLSTimeout: float64(TLS_TIMEOUT) / float64(time.Second), BufSize: DEFAULT_BUF_SIZE, } @@ -85,6 +86,7 @@ func TestTLSConfigFile(t *testing.T) { Username: "derek", Password: "buckley", AuthTimeout: 1.0, + TLSTimeout: 0.5, } opts, err := ProcessConfigFile("./configs/tls.conf") if err != nil { @@ -100,13 +102,8 @@ func TestTLSConfigFile(t *testing.T) { golden, opts) } // Now check TLSConfig a bit more closely - // The default CipherSuites - ciphers := []uint16{ - // tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - // tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - } + // CipherSuites + ciphers := defaultCipherSuites() if !reflect.DeepEqual(tlsConfig.CipherSuites, ciphers) { t.Fatalf("Got incorrect cipher suite list: [%+v]", tlsConfig.CipherSuites) } @@ -150,8 +147,6 @@ func TestTLSConfigFile(t *testing.T) { tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - //tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - //tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, } if !reflect.DeepEqual(tlsConfig.CipherSuites, ciphers) { @@ -167,7 +162,7 @@ func TestTLSConfigFile(t *testing.T) { // Test an empty cipher entry in a config file. opts, err = ProcessConfigFile("./configs/tls_empty_cipher.conf") if err == nil { - t.Fatalf("Did not receive an error from a empty cipher.") + t.Fatalf("Did not receive an error from empty cipher_suites.") } } diff --git a/server/route.go b/server/route.go index 62132060..d79390c7 100644 --- a/server/route.go +++ b/server/route.go @@ -160,14 +160,31 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL) *client { } conn := c.nc.(*tls.Conn) - err := conn.Handshake() - if err != nil { + + // Setup the timeout + ttl := secondsToDuration(s.opts.ClusterTLSTimeout) + time.AfterFunc(ttl, func() { tlsTimeout(c, conn) }) + conn.SetReadDeadline(time.Now().Add(ttl)) + + c.mu.Unlock() + if err := conn.Handshake(); err != nil { c.Debugf("TLS route handshake error: %v", err) + c.sendErr("Secure Connection - TLS Required") c.closeConnection() return nil } + // Reset the read deadline + conn.SetReadDeadline(time.Time{}) + + // Re-Grab lock + c.mu.Lock() + // Rewrap bw c.bw = bufio.NewWriterSize(c.nc, s.opts.BufSize) + + c.Debugf("TLS handshake complete") + cs := conn.ConnectionState() + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) } // Queue Connect proto if we solicited the connection. @@ -366,7 +383,7 @@ func (s *Server) routeAcceptLoop(ch chan struct{}) { continue } tmpDelay = ACCEPT_MIN_SLEEP - s.createRoute(conn, nil) + go s.createRoute(conn, nil) } Debugf("Router accept loop exiting..") s.done <- true @@ -415,7 +432,7 @@ func (s *Server) reConnectToRoute(rUrl *url.URL) { } func (s *Server) connectToRoute(rUrl *url.URL) { - for s.isRunning() { + for s.isRunning() && rUrl != nil { Debugf("Trying to connect to route on %s", rUrl.Host) conn, err := net.DialTimeout("tcp", rUrl.Host, DEFAULT_ROUTE_DIAL) if err != nil { diff --git a/server/server.go b/server/server.go index d5aa685a..fc5897be 100644 --- a/server/server.go +++ b/server/server.go @@ -294,6 +294,11 @@ func (s *Server) AcceptLoop() { return } + // Alert of TLS enabled. + if s.opts.TLSConfig != nil { + Noticef("TLS required for client connections") + } + Noticef("gnatsd is ready") // Setup state that can enable shutdown @@ -333,7 +338,7 @@ func (s *Server) AcceptLoop() { continue } tmpDelay = ACCEPT_MIN_SLEEP - s.createClient(conn) + go s.createClient(conn) } Noticef("Server Exiting..") s.done <- true @@ -424,19 +429,6 @@ func (s *Server) createClient(conn net.Conn) *client { // Send our information. s.sendInfo(c, info) - // Check for TLS - if tlsRequired { - c.Debugf("Starting TLS client connection handshake") - c.nc = tls.Server(c.nc, s.opts.TLSConfig) - conn := c.nc.(*tls.Conn) - err := conn.Handshake() - if err != nil { - c.Debugf("TLS handshake error: %v", err) - } - // Rewrap bw - c.bw = bufio.NewWriterSize(c.nc, s.opts.BufSize) - } - // Unlock to register c.mu.Unlock() @@ -445,9 +437,113 @@ func (s *Server) createClient(conn net.Conn) *client { s.clients[c.cid] = c s.mu.Unlock() + // Check for TLS + if tlsRequired { + // Re-Grab lock + c.mu.Lock() + + c.Debugf("Starting TLS client connection handshake") + c.nc = tls.Server(c.nc, s.opts.TLSConfig) + conn := c.nc.(*tls.Conn) + + // Setup the timeout + ttl := secondsToDuration(s.opts.TLSTimeout) + time.AfterFunc(ttl, func() { tlsTimeout(c, conn) }) + conn.SetReadDeadline(time.Now().Add(ttl)) + + // Force handshake + c.mu.Unlock() + if err := conn.Handshake(); err != nil { + c.Debugf("TLS handshake error: %v", err) + c.sendErr("Secure Connection - TLS Required") + c.closeConnection() + return nil + } + // Reset the read deadline + conn.SetReadDeadline(time.Time{}) + + // Re-Grab lock + c.mu.Lock() + + // Rewrap bw + c.bw = bufio.NewWriterSize(c.nc, s.opts.BufSize) + + c.Debugf("TLS handshake complete") + cs := conn.ConnectionState() + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) + c.mu.Unlock() + } + return c } +// Handle closing down a connection when the handshake has timedout. +func tlsTimeout(c *client, conn *tls.Conn) { + c.mu.Lock() + nc := c.nc + c.mu.Unlock() + // Check if already closed + if nc == nil { + return + } + cs := conn.ConnectionState() + if cs.HandshakeComplete == false { + c.Debugf("TLS handshake timeout") + c.sendErr("Secure Connection - TLS Required") + c.closeConnection() + } +} + +// Seems silly we have to write these +func tlsVersion(ver uint16) string { + switch ver { + case tls.VersionTLS10: + return "1.0" + case tls.VersionTLS11: + return "1.1" + case tls.VersionTLS12: + return "1.2" + } + return fmt.Sprintf("Unknown [%x]", ver) +} + +// We use hex here so we don't need multiple versions +func tlsCipher(cs uint16) string { + switch cs { + case 0x0005: + return "TLS_RSA_WITH_RC4_128_SHA" + case 0x000a: + return "TLS_RSA_WITH_3DES_EDE_CBC_SHA" + case 0x002f: + return "TLS_RSA_WITH_AES_128_CBC_SHA" + case 0x0035: + return "TLS_RSA_WITH_AES_256_CBC_SHA" + case 0xc007: + return "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA" + case 0xc009: + return "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" + case 0xc00a: + return "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" + case 0xc011: + return "TLS_ECDHE_RSA_WITH_RC4_128_SHA" + case 0xc012: + return "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA" + case 0xc013: + return "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" + case 0xc014: + return "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" + case 0xc02f: + return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + case 0xc02b: + return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + case 0xc030: + return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" + case 0xc02c: + return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + } + return fmt.Sprintf("Unknown [%x]", cs) +} + // Assume the lock is held upon entry. func (s *Server) sendInfo(c *client, info []byte) { c.nc.Write(info) diff --git a/server/usage.go b/server/usage.go index 6f7f4b7c..0839c982 100644 --- a/server/usage.go +++ b/server/usage.go @@ -18,8 +18,8 @@ Server Options: Logging Options: -l, --log FILE File to redirect log output -T, --logtime Timestamp log entries (default: true) - -s, --syslog Enable syslog as log method. - -r, --remote_syslog Syslog server addr (udp://localhost:514). + -s, --syslog Enable syslog as log method + -r, --remote_syslog Syslog server addr (udp://localhost:514) -D, --debug Enable debugging output -V, --trace Trace the raw protocol -DV Debug and Trace @@ -28,6 +28,13 @@ Authorization Options: --user user User required for connections --pass password Password required for connections +TLS Options: + --tls Enable TLS, do not verify clients (default: false) + --tlscert FILE Server certificate file + --tlskey FILE Private key for server certificate + --tlsverify Enable TLS, very client certificates + --tlscacert client Client certificate CA for verification + Cluster Options: --routes [rurl-1, rurl-2] Routes to solicit and connect diff --git a/test/configs/tls.conf b/test/configs/tls.conf index ac6a9aa2..66294347 100644 --- a/test/configs/tls.conf +++ b/test/configs/tls.conf @@ -9,6 +9,8 @@ tls { cert_file: "./configs/certs/server-cert.pem" # Server private key key_file: "./configs/certs/server-key.pem" + # Specified time for handshake to complete + timeout: 0.25 } authorization { diff --git a/test/tls_test.go b/test/tls_test.go index 415e193a..cf840692 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -3,11 +3,15 @@ package test import ( + "bufio" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" + "net" + "strings" "testing" + "time" "github.com/nats-io/nats" ) @@ -29,21 +33,6 @@ func TestTLSConnection(t *testing.T) { t.Fatalf("Expected error trying to connect to secure server with no auth") } - nc, err = nats.SecureConnect(nurl) - if err != nil { - t.Fatalf("Got an error on SecureConnect: %+v\n", err) - } - subj := "foo-tls" - sub, _ := nc.SubscribeSync(subj) - - nc.Publish(subj, []byte("We are Secure!")) - nc.Flush() - nmsgs, _ := sub.QueuedMsgs() - if nmsgs != 1 { - t.Fatalf("Expected to receive a message over the TLS connection") - } - defer nc.Close() - // Now do more advanced checking, verifying servername and using rootCA. // Setup our own TLSConfig using RootCA from our self signed cert. rootPEM, err := ioutil.ReadFile("./configs/certs/ca.pem") @@ -71,8 +60,17 @@ func TestTLSConnection(t *testing.T) { if err != nil { t.Fatalf("Got an error on Connect with Secure Options: %+v\n", err) } - nc.Flush() defer nc.Close() + + subj := "foo-tls" + sub, _ := nc.SubscribeSync(subj) + + nc.Publish(subj, []byte("We are Secure!")) + nc.Flush() + nmsgs, _ := sub.QueuedMsgs() + if nmsgs != 1 { + t.Fatalf("Expected to receive a message over the TLS connection") + } } func TestTLSClientCertificate(t *testing.T) { @@ -129,3 +127,37 @@ func TestTLSClientCertificate(t *testing.T) { nc.Flush() defer nc.Close() } + +func TestTLSConnectionTimeout(t *testing.T) { + srv, opts := RunServerWithConfig("./configs/tls.conf") + defer srv.Shutdown() + + // Dial with normal TCP + endpoint := fmt.Sprintf("%s:%d", opts.Host, opts.Port) + conn, err := net.Dial("tcp", endpoint) + if err != nil { + t.Fatalf("Could not connect to %q", endpoint) + } + defer conn.Close() + + // Read deadlines + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + + // Read the INFO string. + br := bufio.NewReader(conn) + info, err := br.ReadString('\n') + if err != nil { + t.Fatalf("Failed to read INFO - %v", err) + } + if !strings.HasPrefix(info, "INFO ") { + t.Fatalf("INFO response incorrect: %s\n", info) + } + wait := time.Duration(opts.TLSTimeout * float64(time.Second)) + time.Sleep(wait) + // Read deadlines + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + tlsErr, err := br.ReadString('\n') + if err == nil && !strings.Contains(tlsErr, "-ERR 'Secure Connection - TLS Required") { + t.Fatalf("TLS Timeout response incorrect: %q\n", tlsErr) + } +}