diff --git a/server/auth.go b/server/auth.go index 601e28a4..7043b2cc 100644 --- a/server/auth.go +++ b/server/auth.go @@ -20,6 +20,7 @@ import ( "encoding/base64" "fmt" "net" + "net/url" "regexp" "strings" "time" @@ -418,7 +419,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo } else if hasUsers { // Check if we are tls verify and are mapping users from the client_certificate. if tlsMap { - authorized := checkClientTLSCertSubject(c, func(u string, certRDN *ldap.DN) (string, bool) { + authorized := checkClientTLSCertSubject(c, func(u string, certRDN *ldap.DN, _ bool) (string, bool) { // First do literal lookup using the resulting string representation // of RDNSequence as implemented by the pkix package from Go. if u != "" { @@ -667,7 +668,7 @@ func getTLSAuthDCs(rdns *pkix.RDNSequence) string { return strings.Join(dcs, ",") } -type tlsMapAuthFn func(string, *ldap.DN) (string, bool) +type tlsMapAuthFn func(string, *ldap.DN, bool) (string, bool) func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { tlsState := c.GetTLSConnectionState() @@ -696,7 +697,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { switch { case hasEmailAddresses: for _, u := range cert.EmailAddresses { - if match, ok := fn(u, nil); ok { + if match, ok := fn(u, nil, false); ok { c.Debugf("Using email found in cert for auth [%q]", match) return true } @@ -704,7 +705,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { fallthrough case hasSANs: for _, u := range cert.DNSNames { - if match, ok := fn(u, nil); ok { + if match, ok := fn(u, nil, true); ok { c.Debugf("Using SAN found in cert for auth [%q]", match) return true } @@ -712,7 +713,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { fallthrough case hasURIs: for _, u := range cert.URIs { - if match, ok := fn(u.String(), nil); ok { + if match, ok := fn(u.String(), nil, false); ok { c.Debugf("Using URI found in cert for auth [%q]", match) return true } @@ -726,7 +727,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { // Match that follows original order from the subject takes precedence. dn, err := ldap.FromCertSubject(cert.Subject) if err == nil { - if match, ok := fn("", dn); ok { + if match, ok := fn("", dn, false); ok { c.Debugf("Using DistinguishedNameMatch for auth [%q]", match) return true } @@ -745,7 +746,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { dcs := getTLSAuthDCs(&rdns) if len(dcs) > 0 { u := strings.Join([]string{rdn, dcs}, ",") - if match, ok := fn(u, nil); ok { + if match, ok := fn(u, nil, false); ok { c.Debugf("Using RDNSequence for auth [%q]", match) return true } @@ -755,7 +756,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { // If no match, then use the string representation of the RDNSequence // from the subject without the domainComponents. - if match, ok := fn(rdn, nil); ok { + if match, ok := fn(rdn, nil, false); ok { c.Debugf("Using certificate subject for auth [%q]", match) return true } @@ -764,6 +765,38 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { return false } +func dnsAltNameLabels(dnsAltName string) []string { + return strings.Split(strings.ToLower(dnsAltName), ".") +} + +// Check DNS name according to https://tools.ietf.org/html/rfc6125#section-6.4.1 +func dnsAltNameMatches(dnsAltNameLabels []string, urls []*url.URL) bool { +URLS: + for _, url := range urls { + if url == nil { + continue URLS + } + hostLabels := strings.Split(strings.ToLower(url.Hostname()), ".") + // Following https://tools.ietf.org/html/rfc6125#section-6.4.3, should not => will not, may => will not + // The wilcard * never matches multiple label and only matches the left most label. + if len(hostLabels) != len(dnsAltNameLabels) { + continue URLS + } + i := 0 + // only match wildcard on left most label + if dnsAltNameLabels[0] == "*" { + i++ + } + for ; i < len(dnsAltNameLabels); i++ { + if dnsAltNameLabels[i] != hostLabels[i] { + continue URLS + } + } + return true + } + return false +} + // checkRouterAuth checks optional router authorization which can be nil or username/password. func (s *Server) isRouterAuthorized(c *client) bool { // Snapshot server options. @@ -775,14 +808,25 @@ func (s *Server) isRouterAuthorized(c *client) bool { return s.opts.CustomRouterAuthentication.Check(c) } - if opts.Cluster.Username == "" { - return true + if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnwonURLs { + return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) { + if user == "" { + return "", false + } + if opts.Cluster.TLSCheckKnwonURLs && isDNSAltName { + if dnsAltNameMatches(dnsAltNameLabels(user), opts.Routes) { + return "", true + } + } + if opts.Cluster.TLSMap && opts.Cluster.Username == user { + return "", true + } + return "", false + }) } - if opts.Cluster.TLSMap { - return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN) (string, bool) { - return "", opts.Cluster.Username == user - }) + if opts.Cluster.Username == "" { + return true } if opts.Cluster.Username != c.opts.Username { @@ -798,17 +842,32 @@ func (s *Server) isRouterAuthorized(c *client) bool { func (s *Server) isGatewayAuthorized(c *client) bool { // Snapshot server options. opts := s.getOpts() - if opts.Gateway.Username == "" { - return true - } // Check whether TLS map is enabled, otherwise use single user/pass. - if opts.Gateway.TLSMap { - return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN) (string, bool) { - return "", opts.Gateway.Username == user + if opts.Gateway.TLSMap || opts.Gateway.TLSCheckKnownURLs { + return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) { + if user == "" { + return "", false + } + if opts.Gateway.TLSCheckKnownURLs && isDNSAltName { + labels := dnsAltNameLabels(user) + for _, gw := range opts.Gateway.Gateways { + if gw != nil && dnsAltNameMatches(labels, gw.URLs) { + return "", true + } + } + } + if opts.Gateway.TLSMap && opts.Gateway.Username == user { + return "", true + } + return "", false }) } + if opts.Gateway.Username == "" { + return true + } + if opts.Gateway.Username != c.opts.Username { return false } @@ -854,7 +913,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { } else if len(opts.LeafNode.Users) > 0 { if opts.LeafNode.TLSMap { var user *User - found := checkClientTLSCertSubject(c, func(u string, _ *ldap.DN) (string, bool) { + found := checkClientTLSCertSubject(c, func(u string, _ *ldap.DN, _ bool) (string, bool) { // This is expected to be a very small array. for _, usr := range opts.LeafNode.Users { if u == usr.Username { diff --git a/server/auth_test.go b/server/auth_test.go index 031c70ec..1fb3acef 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -14,6 +14,7 @@ package server import ( + "net/url" "reflect" "strings" "testing" @@ -154,3 +155,60 @@ func TestUserUnknownAllowedConnectionType(t *testing.T) { } } } + +func TestDNSAltNameMatching(t *testing.T) { + for idx, test := range []struct { + altName string + urls []string + match bool + }{ + {"foo", []string{"FOO"}, true}, + {"foo", []string{".."}, false}, + {"foo", []string{"."}, false}, + {"Foo", []string{"foO"}, true}, + {"FOO", []string{"foo"}, true}, + {"foo1", []string{"bar"}, false}, + {"multi", []string{"m", "mu", "mul", "multi"}, true}, + {"multi", []string{"multi", "m", "mu", "mul"}, true}, + {"foo.bar", []string{"foo", "foo.bar.bar", "foo.baz"}, false}, + {"foo.Bar", []string{"foo", "bar.foo", "Foo.Bar"}, true}, + {"foo.*", []string{"foo", "bar.foo", "Foo.Bar"}, false}, // only match left most + {"f*.bar", []string{"foo", "bar.foo", "Foo.Bar"}, false}, + {"*.bar", []string{"foo.bar"}, true}, + {"*", []string{"baz.bar", "bar", "z.y"}, true}, + {"*", []string{"bar"}, true}, + {"*", []string{"."}, false}, + {"*", []string{""}, true}, + {"*", []string{"*"}, true}, + {"bar.*", []string{"bar.*"}, true}, + {"*.Y-X-red-mgmt.default.svc", []string{"A.Y-X-red-mgmt.default.svc"}, true}, + {"*.Y-X-green-mgmt.default.svc", []string{"A.Y-X-green-mgmt.default.svc"}, true}, + {"*.Y-X-blue-mgmt.default.svc", []string{"A.Y-X-blue-mgmt.default.svc"}, true}, + {"Y-X-red-mgmt", []string{"Y-X-red-mgmt"}, true}, + {"Y-X-red-mgmt", []string{"X-X-red-mgmt"}, false}, + {"Y-X-red-mgmt", []string{"Y-X-green-mgmt"}, false}, + {"Y-X-red-mgmt", []string{"Y"}, false}, + {"Y-X-red-mgmt", []string{"Y-X"}, false}, + {"Y-X-red-mgmt", []string{"Y-X-red"}, false}, + {"Y-X-red-mgmt", []string{"X-red-mgmt"}, false}, + {"Y-X-green-mgmt", []string{"Y-X-green-mgmt"}, true}, + {"Y-X-blue-mgmt", []string{"Y-X-blue-mgmt"}, true}, + {"connect.Y.local", []string{"connect.Y.local"}, true}, + {"connect.Y.local", []string{".Y.local"}, false}, + {"connect.Y.local", []string{"..local"}, false}, + {"gcp.Y.local", []string{"gcp.Y.local"}, true}, + {"uswest1.gcp.Y.local", []string{"uswest1.gcp.Y.local"}, true}, + } { + urlSet := make([]*url.URL, len(test.urls)) + for i, u := range test.urls { + var err error + urlSet[i], err = url.Parse("nats://" + u) + if err != nil { + t.Fatal(err) + } + } + if dnsAltNameMatches(dnsAltNameLabels(test.altName), urlSet) != test.match { + t.Fatal("Test", idx, "Match miss match, expected:", test.match) + } + } +} diff --git a/server/config_check_test.go b/server/config_check_test.go index c6e9ce30..166aa761 100644 --- a/server/config_check_test.go +++ b/server/config_check_test.go @@ -360,6 +360,19 @@ func TestConfigCheck(t *testing.T) { errorLine: 4, errorPos: 5, }, + { + name: "verify_cert_and_check_known_urls not support for clients", + config: ` + tls = { + cert_file: "configs/certs/server.pem" + key_file: "configs/certs/key.pem" + verify_cert_and_check_known_urls: true + } + `, + err: errors.New("verify_cert_and_check_known_urls not supported in this context"), + errorLine: 5, + errorPos: 10, + }, { name: "when unknown option is in cluster config with defined routes", config: ` @@ -1140,6 +1153,25 @@ func TestConfigCheck(t *testing.T) { errorLine: 0, errorPos: 0, }, + { + name: "verify_cert_and_check_known_urls do not work for leaf nodes", + config: ` + leafnodes { + remotes = [ + { + url: "tls://nats:7422" + tls { + timeout: 0.01 + verify_cert_and_check_known_urls: true + } + } + ] + }`, + //Unexpected error after processing config: /var/folders/9h/6g_c9l6n6bb8gp331d_9y0_w0000gn/T/057996446:8:5: + err: errors.New("verify_cert_and_check_known_urls not supported in this context"), + errorLine: 8, + errorPos: 5, + }, { name: "when leafnode remotes use wrong type", config: ` @@ -1360,6 +1392,21 @@ func TestConfigCheck(t *testing.T) { errorLine: 3, errorPos: 21, }, + { + name: "verify_cert_and_check_known_urls not support for websockets", + config: ` + websocket { + tls { + cert_file: "configs/certs/server.pem" + key_file: "configs/certs/key.pem" + verify_cert_and_check_known_urls: true + } + } + `, + err: fmt.Errorf("verify_cert_and_check_known_urls not supported in this context"), + errorLine: 6, + errorPos: 10, + }, } checkConfig := func(config string) error { diff --git a/server/opts.go b/server/opts.go index 817f198d..32802133 100644 --- a/server/opts.go +++ b/server/opts.go @@ -57,20 +57,21 @@ func NoErrOnUnknownFields(noError bool) { // NOTE: This structure is no longer used for monitoring endpoints // and json tags are deprecated and may be removed in the future. type ClusterOpts struct { - Name string `json:"-"` - Host string `json:"addr,omitempty"` - Port int `json:"cluster_port,omitempty"` - Username string `json:"-"` - Password string `json:"-"` - AuthTimeout float64 `json:"auth_timeout,omitempty"` - Permissions *RoutePermissions `json:"-"` - TLSTimeout float64 `json:"-"` - TLSConfig *tls.Config `json:"-"` - TLSMap bool `json:"-"` - ListenStr string `json:"-"` - Advertise string `json:"-"` - NoAdvertise bool `json:"-"` - ConnectRetries int `json:"-"` + Name string `json:"-"` + Host string `json:"addr,omitempty"` + Port int `json:"cluster_port,omitempty"` + Username string `json:"-"` + Password string `json:"-"` + AuthTimeout float64 `json:"auth_timeout,omitempty"` + Permissions *RoutePermissions `json:"-"` + TLSTimeout float64 `json:"-"` + TLSConfig *tls.Config `json:"-"` + TLSMap bool `json:"-"` + TLSCheckKnwonURLs bool `json:"-"` + ListenStr string `json:"-"` + Advertise string `json:"-"` + NoAdvertise bool `json:"-"` + ConnectRetries int `json:"-"` // Not exported (used in tests) resolver netResolver @@ -80,19 +81,20 @@ type ClusterOpts struct { // NOTE: This structure is no longer used for monitoring endpoints // and json tags are deprecated and may be removed in the future. type GatewayOpts struct { - Name string `json:"name"` - Host string `json:"addr,omitempty"` - Port int `json:"port,omitempty"` - Username string `json:"-"` - Password string `json:"-"` - AuthTimeout float64 `json:"auth_timeout,omitempty"` - TLSConfig *tls.Config `json:"-"` - TLSTimeout float64 `json:"tls_timeout,omitempty"` - TLSMap bool `json:"-"` - Advertise string `json:"advertise,omitempty"` - ConnectRetries int `json:"connect_retries,omitempty"` - Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` - RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster + Name string `json:"name"` + Host string `json:"addr,omitempty"` + Port int `json:"port,omitempty"` + Username string `json:"-"` + Password string `json:"-"` + AuthTimeout float64 `json:"auth_timeout,omitempty"` + TLSConfig *tls.Config `json:"-"` + TLSTimeout float64 `json:"tls_timeout,omitempty"` + TLSMap bool `json:"-"` + TLSCheckKnownURLs bool `json:"-"` + Advertise string `json:"advertise,omitempty"` + ConnectRetries int `json:"connect_retries,omitempty"` + Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` + RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster // Not exported, for tests. resolver netResolver @@ -391,15 +393,16 @@ type authorization struct { // TLSConfigOpts holds the parsed tls config information, // used with flag parsing type TLSConfigOpts struct { - CertFile string - KeyFile string - CaFile string - Verify bool - Insecure bool - Map bool - Timeout float64 - Ciphers []uint16 - CurvePreferences []tls.CurveID + CertFile string + KeyFile string + CaFile string + Verify bool + Insecure bool + Map bool + TLSCheckKnownURLs bool + Timeout float64 + Ciphers []uint16 + CurvePreferences []tls.CurveID } var tlsUsage = ` @@ -745,7 +748,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error case "ping_max": o.MaxPingsOut = int(v.(int64)) case "tls": - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, true) if err != nil { *errors = append(*errors, err) return @@ -924,7 +927,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error *errors = append(*errors, err) } case "resolver_tls": - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, true) if err != nil { *errors = append(*errors, err) return @@ -1160,6 +1163,7 @@ func parseCluster(v interface{}, opts *Options, errors *[]error, warnings *[]err opts.Cluster.TLSConfig = config opts.Cluster.TLSTimeout = tlsopts.Timeout opts.Cluster.TLSMap = tlsopts.Map + opts.Cluster.TLSCheckKnwonURLs = tlsopts.TLSCheckKnownURLs case "cluster_advertise", "advertise": opts.Cluster.Advertise = mv.(string) case "no_advertise": @@ -1275,6 +1279,7 @@ func parseGateway(v interface{}, o *Options, errors *[]error, warnings *[]error) o.Gateway.TLSConfig = config o.Gateway.TLSTimeout = tlsopts.Timeout o.Gateway.TLSMap = tlsopts.Map + o.Gateway.TLSCheckKnownURLs = tlsopts.TLSCheckKnownURLs case "advertise": o.Gateway.Advertise = mv.(string) case "connect_retries": @@ -1481,7 +1486,7 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e case "reconnect", "reconnect_delay", "reconnect_interval": opts.LeafNode.ReconnectInterval = time.Duration(int(mv.(int64))) * time.Second case "tls": - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, true) if err != nil { *errors = append(*errors, err) continue @@ -1678,7 +1683,7 @@ func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([] } remote.Credentials = p case "tls": - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, true) if err != nil { *errors = append(*errors, err) continue @@ -1733,7 +1738,7 @@ func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([] // Parse TLS and returns a TLSConfig and TLSTimeout. // Used by cluster and gateway parsing. func getTLSConfig(tk token) (*tls.Config, *TLSConfigOpts, error) { - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, false) if err != nil { return nil, nil, err } @@ -3203,7 +3208,7 @@ func parseCurvePreferences(curveName string) (tls.CurveID, error) { } // Helper function to parse TLS configs. -func parseTLS(v interface{}) (t *TLSConfigOpts, retErr error) { +func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error) { var ( tlsm map[string]interface{} tc = TLSConfigOpts{} @@ -3251,8 +3256,22 @@ func parseTLS(v interface{}) (t *TLSConfigOpts, retErr error) { if !ok { return nil, &configErr{tk, "error parsing tls config, expected 'verify_and_map' to be a boolean"} } - tc.Verify = verify + if verify { + tc.Verify = verify + } tc.Map = verify + case "verify_cert_and_check_known_urls": + verify, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'verify_cert_and_check_known_urls' to be a boolean"} + } + if verify && isClientCtx { + return nil, &configErr{tk, "verify_cert_and_check_known_urls not supported in this context"} + } + if verify { + tc.Verify = verify + } + tc.TLSCheckKnownURLs = verify case "cipher_suites": ra := mv.([]interface{}) if len(ra) == 0 { @@ -3408,7 +3427,7 @@ func parseWebsocket(v interface{}, o *Options, errors *[]error, warnings *[]erro case "no_tls": o.Websocket.NoTLS = mv.(bool) case "tls": - tc, err := parseTLS(tk) + tc, err := parseTLS(tk, true) if err != nil { *errors = append(*errors, err) continue diff --git a/test/tls_test.go b/test/tls_test.go index 633d88f5..17ceac39 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -786,6 +786,175 @@ func TestTLSGatewaysCertificateCNBasedAuth(t *testing.T) { } } +func TestTLSRoutesCertificateImplicitAllowPass(t *testing.T) { + testTLSRoutesCertificateImplicitAllow(t, true) +} + +func TestTLSRoutesCertificateImplicitAllowFail(t *testing.T) { + testTLSRoutesCertificateImplicitAllow(t, false) +} + +func testTLSRoutesCertificateImplicitAllow(t *testing.T, pass bool) { + t.Helper() + // Base config for the servers + cfg, err := ioutil.TempFile("", "cfg") + if err != nil { + t.Fatal(err) + } + defer os.Remove(cfg.Name()) + cfg.WriteString(fmt.Sprintf(` + cluster { + tls { + cert_file = "./configs/certs/tlsauth/server.pem" + key_file = "./configs/certs/tlsauth/server-key.pem" + ca_file = "./configs/certs/tlsauth/ca.pem" + verify_cert_and_check_known_urls = true + insecure = %t + timeout = 1 + } + } + `, !pass)) // set insecure to skip verification on the outgoing end + if err = cfg.Sync(); err != nil { + t.Fatal(err) + } + + optsA := LoadConfig(cfg.Name()) + optsB := LoadConfig(cfg.Name()) + + routeURLs := "nats://localhost:9935, nats://localhost:9936" + if !pass { + routeURLs = "nats://127.0.0.1:9935, nats://127.0.0.1:9936" + } + optsA.Host = "127.0.0.1" + optsA.Port = 9335 + optsA.Cluster.Name = "xyz" + optsA.Cluster.Host = optsA.Host + optsA.Cluster.Port = 9935 + optsA.Routes = server.RoutesFromStr(routeURLs) + optsA.NoSystemAccount = true + srvA := RunServer(optsA) + defer srvA.Shutdown() + + optsB.Host = "127.0.0.1" + optsB.Port = 9336 + optsB.Cluster.Name = "xyz" + optsB.Cluster.Host = optsB.Host + optsB.Cluster.Port = 9936 + optsB.Routes = server.RoutesFromStr(routeURLs) + optsB.NoSystemAccount = true + srvB := RunServer(optsB) + defer srvB.Shutdown() + + // srvC is not connected to srvA and srvB due to wrong cert + if pass { + checkNumRoutes(t, srvA, 1) + checkNumRoutes(t, srvB, 1) + } else { + time.Sleep(1 * time.Second) // the fail case uses the IP, so a short wait is sufficient + if srvA.NumRoutes() != 0 || srvB.NumRoutes() != 0 { + t.Fatal("No route connection expected") + } + } +} + +func TestTLSGatewaysCertificateImplicitAllowPass(t *testing.T) { + testTLSGatewaysCertificateImplicitAllow(t, true) +} + +func TestTLSGatewaysCertificateImplicitAllowFail(t *testing.T) { + testTLSGatewaysCertificateImplicitAllow(t, false) +} + +func testTLSGatewaysCertificateImplicitAllow(t *testing.T, pass bool) { + t.Helper() + // Base config for the servers + cfg, err := ioutil.TempFile("", "cfg") + if err != nil { + t.Fatal(err) + } + defer os.Remove(cfg.Name()) + cfg.WriteString(fmt.Sprintf(` + gateway { + tls { + cert_file = "./configs/certs/tlsauth/server.pem" + key_file = "./configs/certs/tlsauth/server-key.pem" + ca_file = "./configs/certs/tlsauth/ca.pem" + verify_cert_and_check_known_urls = true + insecure = %t + timeout = 1 + } + } + `, !pass)) // set insecure to skip verification on the outgoing end + if err = cfg.Sync(); err != nil { + t.Fatal(err) + } + + optsA := LoadConfig(cfg.Name()) + optsB := LoadConfig(cfg.Name()) + + urlA := "nats://localhost:9995" + urlB := "nats://localhost:9996" + if !pass { + urlA = "nats://127.0.0.1:9995" + urlB = "nats://127.0.0.1:9996" + } + + gwA, err := url.Parse(urlA) + if err != nil { + t.Fatal(err) + } + gwB, err := url.Parse(urlB) + if err != nil { + t.Fatal(err) + } + + optsA.Host = "127.0.0.1" + optsA.Port = -1 + optsA.Gateway.Name = "A" + optsA.Gateway.Port = 9995 + + optsB.Host = "127.0.0.1" + optsB.Port = -1 + optsB.Gateway.Name = "B" + optsB.Gateway.Port = 9996 + + gateways := make([]*server.RemoteGatewayOpts, 2) + gateways[0] = &server.RemoteGatewayOpts{ + Name: optsA.Gateway.Name, + URLs: []*url.URL{gwA}, + } + gateways[1] = &server.RemoteGatewayOpts{ + Name: optsB.Gateway.Name, + URLs: []*url.URL{gwB}, + } + + optsA.Gateway.Gateways = gateways + optsB.Gateway.Gateways = gateways + + server.SetGatewaysSolicitDelay(100 * time.Millisecond) + defer server.ResetGatewaysSolicitDelay() + + srvA := RunServer(optsA) + defer srvA.Shutdown() + + srvB := RunServer(optsB) + defer srvB.Shutdown() + + // Because we need to use "localhost" in the gw URLs (to match + // hostname in the user/CN), the server may try to connect to + // a [::1], etc.. that may or may not work, so give a lot of + // time for that to complete ok. + if pass { + waitForOutboundGateways(t, srvA, 1, 5*time.Second) + waitForOutboundGateways(t, srvB, 1, 5*time.Second) + } else { + time.Sleep(1 * time.Second) // the fail case uses the IP, so a short wait is sufficient + if srvA.NumOutboundGateways() != 0 || srvB.NumOutboundGateways() != 0 { + t.Fatal("No outbound gateway connection expected") + } + } +} + func TestTLSVerifyClientCertificate(t *testing.T) { srv, opts := RunServerWithConfig("./configs/tlsverify_noca.conf") defer srv.Shutdown()