Files
nats-server/server/leafnode_test.go
Ivan Kozlovic afb5086f17 [CHANGED] LeafNode: remotes from same server binding to same hub account
Previously, the server would reject a second remote leafnode connection
from the same server if it was binding to the same account on the hub
even if the remote was using different local accounts.

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>
2023-06-20 18:44:52 -06:00

6806 lines
181 KiB
Go

// Copyright 2019-2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"fmt"
"math/rand"
"net"
"net/url"
"reflect"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/nats-io/nkeys"
"github.com/klauspost/compress/s2"
jwt "github.com/nats-io/jwt/v2"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats-server/v2/internal/testhelper"
)
type captureLeafNodeRandomIPLogger struct {
DummyLogger
ch chan struct{}
ips [3]int
}
func (c *captureLeafNodeRandomIPLogger) Debugf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
if strings.Contains(msg, "hostname_to_resolve") {
ippos := strings.Index(msg, "127.0.0.")
if ippos != -1 {
n := int(msg[ippos+8] - '1')
c.ips[n]++
for _, v := range c.ips {
if v < 2 {
return
}
}
// All IPs got at least some hit, we are done.
c.ch <- struct{}{}
}
}
}
func TestLeafNodeRandomIP(t *testing.T) {
u, err := url.Parse("nats://hostname_to_resolve:1234")
if err != nil {
t.Fatalf("Error parsing: %v", err)
}
resolver := &myDummyDNSResolver{ips: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}}
o := DefaultOptions()
o.Host = "127.0.0.1"
o.Port = -1
o.LeafNode.Port = 0
o.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
o.LeafNode.ReconnectInterval = 50 * time.Millisecond
o.LeafNode.resolver = resolver
o.LeafNode.dialTimeout = 15 * time.Millisecond
s := RunServer(o)
defer s.Shutdown()
l := &captureLeafNodeRandomIPLogger{ch: make(chan struct{})}
s.SetLogger(l, true, true)
select {
case <-l.ch:
case <-time.After(3 * time.Second):
t.Fatalf("Does not seem to have used random IPs")
}
}
func TestLeafNodeRandomRemotes(t *testing.T) {
// 16! possible permutations.
orderedURLs := make([]*url.URL, 0, 16)
for i := 0; i < cap(orderedURLs); i++ {
orderedURLs = append(orderedURLs, &url.URL{
Scheme: "nats-leaf",
Host: fmt.Sprintf("host%d:7422", i),
})
}
o := DefaultOptions()
o.LeafNode.Remotes = []*RemoteLeafOpts{
{NoRandomize: true},
{NoRandomize: false},
}
o.LeafNode.Remotes[0].URLs = make([]*url.URL, cap(orderedURLs))
copy(o.LeafNode.Remotes[0].URLs, orderedURLs)
o.LeafNode.Remotes[1].URLs = make([]*url.URL, cap(orderedURLs))
copy(o.LeafNode.Remotes[1].URLs, orderedURLs)
s := RunServer(o)
defer s.Shutdown()
s.mu.Lock()
r1 := s.leafRemoteCfgs[0]
r2 := s.leafRemoteCfgs[1]
s.mu.Unlock()
r1.RLock()
gotOrdered := r1.urls
r1.RUnlock()
if got, want := len(gotOrdered), len(orderedURLs); got != want {
t.Fatalf("Unexpected rem0 len URLs, got %d, want %d", got, want)
}
// These should be IN order.
for i := range orderedURLs {
if got, want := gotOrdered[i].String(), orderedURLs[i].String(); got != want {
t.Fatalf("Unexpected ordered url, got %s, want %s", got, want)
}
}
r2.RLock()
gotRandom := r2.urls
r2.RUnlock()
if got, want := len(gotRandom), len(orderedURLs); got != want {
t.Fatalf("Unexpected rem1 len URLs, got %d, want %d", got, want)
}
// These should be OUT of order.
var random bool
for i := range orderedURLs {
if gotRandom[i].String() != orderedURLs[i].String() {
random = true
break
}
}
if !random {
t.Fatal("Expected urls to be random")
}
}
type testLoopbackResolver struct{}
func (r *testLoopbackResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
return []string{"127.0.0.1"}, nil
}
func TestLeafNodeTLSWithCerts(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
leaf {
listen: "127.0.0.1:-1"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
cert_file: "../test/configs/certs/tlsauth/server.pem"
key_file: "../test/configs/certs/tlsauth/server-key.pem"
timeout: 2
}
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", o1.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [
{
url: "%s"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
cert_file: "../test/configs/certs/tlsauth/client.pem"
key_file: "../test/configs/certs/tlsauth/client-key.pem"
timeout: 2
}
}
]
}
`, u.String())))
o2, err := ProcessConfigFile(conf2)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
o2.NoLog, o2.NoSigs = true, true
o2.LeafNode.resolver = &testLoopbackResolver{}
s2 := RunServer(o2)
defer s2.Shutdown()
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
if nln := s1.NumLeafNodes(); nln != 1 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
return nil
})
}
func TestLeafNodeTLSRemoteWithNoCerts(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
leaf {
listen: "127.0.0.1:-1"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
cert_file: "../test/configs/certs/tlsauth/server.pem"
key_file: "../test/configs/certs/tlsauth/server-key.pem"
timeout: 2
}
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", o1.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [
{
url: "%s"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
timeout: 5
}
}
]
}
`, u.String())))
o2, err := ProcessConfigFile(conf2)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
if len(o2.LeafNode.Remotes) == 0 {
t.Fatal("Expected at least a single leaf remote")
}
var (
got float64 = o2.LeafNode.Remotes[0].TLSTimeout
expected float64 = 5
)
if got != expected {
t.Fatalf("Expected %v, got: %v", expected, got)
}
o2.NoLog, o2.NoSigs = true, true
o2.LeafNode.resolver = &testLoopbackResolver{}
s2 := RunServer(o2)
defer s2.Shutdown()
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
if nln := s1.NumLeafNodes(); nln != 1 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
return nil
})
// Here we only process options without starting the server
// and without a root CA for the remote.
conf3 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [
{
url: "%s"
tls {
timeout: 10
}
}
]
}
`, u.String())))
o3, err := ProcessConfigFile(conf3)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
if len(o3.LeafNode.Remotes) == 0 {
t.Fatal("Expected at least a single leaf remote")
}
got = o3.LeafNode.Remotes[0].TLSTimeout
expected = 10
if got != expected {
t.Fatalf("Expected %v, got: %v", expected, got)
}
// Here we only process options without starting the server
// and check the default for leafnode remotes.
conf4 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [
{
url: "%s"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
}
}
]
}
`, u.String())))
o4, err := ProcessConfigFile(conf4)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
if len(o4.LeafNode.Remotes) == 0 {
t.Fatal("Expected at least a single leaf remote")
}
got = o4.LeafNode.Remotes[0].TLSTimeout
expected = float64(DEFAULT_LEAF_TLS_TIMEOUT) / float64(time.Second)
if int(got) != int(expected) {
t.Fatalf("Expected %v, got: %v", expected, got)
}
}
type captureErrorLogger struct {
DummyLogger
errCh chan string
}
func (l *captureErrorLogger) Errorf(format string, v ...interface{}) {
select {
case l.errCh <- fmt.Sprintf(format, v...):
default:
}
}
func TestLeafNodeAccountNotFound(t *testing.T) {
ob := DefaultOptions()
ob.LeafNode.Host = "127.0.0.1"
ob.LeafNode.Port = -1
sb := RunServer(ob)
defer sb.Shutdown()
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
oa := DefaultOptions()
oa.Cluster.Name = "xyz"
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
oa.LeafNode.Remotes = []*RemoteLeafOpts{
{
LocalAccount: "foo",
URLs: []*url.URL{u},
},
}
// Expected to fail
if _, err := NewServer(oa); err == nil || !strings.Contains(err.Error(), "local account") {
t.Fatalf("Expected server to fail with error about no local account, got %v", err)
}
oa.Accounts = []*Account{NewAccount("foo")}
sa := RunServer(oa)
defer sa.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 1)}
sa.SetLogger(l, false, false)
checkLeafNodeConnected(t, sa)
// Now simulate account is removed with config reload, or it expires.
sa.accounts.Delete("foo")
// Restart B (with same Port)
sb.Shutdown()
sb = RunServer(ob)
defer sb.Shutdown()
// Wait for report of error
select {
case e := <-l.errCh:
if !strings.Contains(e, "Unable to lookup account") {
t.Fatalf("Expected error about no local account, got %s", e)
}
case <-time.After(2 * time.Second):
t.Fatalf("Did not get the error")
}
// TODO below test is bogus. Instead add the account, do a reload, and make sure the connection works.
// For now, sa would try to recreate the connection for ever.
// Check that lid is increasing...
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
if lid := atomic.LoadUint64(&sa.gcid); lid < 3 {
return fmt.Errorf("Seems like connection was not retried, lid currently only at %d", lid)
}
return nil
})
}
// This test ensures that we can connect using proper user/password
// to a LN URL that was discovered through the INFO protocol.
// We also check that the password doesn't leak to debug/trace logs.
func TestLeafNodeBasicAuthFailover(t *testing.T) {
// Something a little longer than "pwd" to prevent false positives amongst many log lines;
// don't make it complex enough to be subject to %-escaping, we want a simple needle search.
fatalPassword := "pwdfatal"
content := `
listen: "127.0.0.1:-1"
cluster {
name: "abc"
listen: "127.0.0.1:-1"
%s
}
leafnodes {
listen: "127.0.0.1:-1"
authorization {
user: foo
password: %s
timeout: 1
}
}
`
conf := createConfFile(t, []byte(fmt.Sprintf(content, "", fatalPassword)))
sb1, ob1 := RunServerWithConfig(conf)
defer sb1.Shutdown()
conf = createConfFile(t, []byte(fmt.Sprintf(content, fmt.Sprintf("routes: [nats://127.0.0.1:%d]", ob1.Cluster.Port), fatalPassword)))
sb2, _ := RunServerWithConfig(conf)
defer sb2.Shutdown()
checkClusterFormed(t, sb1, sb2)
content = `
port: -1
accounts {
foo {}
}
leafnodes {
listen: "127.0.0.1:-1"
remotes [
{
account: "foo"
url: "nats://foo:%s@127.0.0.1:%d"
}
]
}
`
conf = createConfFile(t, []byte(fmt.Sprintf(content, fatalPassword, ob1.LeafNode.Port)))
sa, _ := RunServerWithConfig(conf)
defer sa.Shutdown()
l := testhelper.NewDummyLogger(100)
sa.SetLogger(l, true, true) // we want debug & trace logs, to check for passwords in them
checkLeafNodeConnected(t, sa)
// Shutdown sb1, sa should reconnect to sb2
sb1.Shutdown()
// Wait a bit to make sure there was a disconnect and attempt to reconnect.
time.Sleep(250 * time.Millisecond)
// Should be able to reconnect
checkLeafNodeConnected(t, sa)
// Look at all our logs for the password; at time of writing it doesn't appear
// but we want to safe-guard against it.
l.CheckForProhibited(t, "fatal password", fatalPassword)
}
func TestLeafNodeRTT(t *testing.T) {
ob := DefaultOptions()
ob.PingInterval = 15 * time.Millisecond
ob.LeafNode.Host = "127.0.0.1"
ob.LeafNode.Port = -1
sb := RunServer(ob)
defer sb.Shutdown()
lnBURL, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
oa := DefaultOptions()
oa.Cluster.Name = "xyz"
oa.PingInterval = 15 * time.Millisecond
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{lnBURL}}}
sa := RunServer(oa)
defer sa.Shutdown()
checkLeafNodeConnected(t, sa)
checkRTT := func(t *testing.T, s *Server) time.Duration {
t.Helper()
var ln *client
s.mu.Lock()
for _, l := range s.leafs {
ln = l
break
}
s.mu.Unlock()
var rtt time.Duration
checkFor(t, 2*firstPingInterval, 15*time.Millisecond, func() error {
ln.mu.Lock()
rtt = ln.rtt
ln.mu.Unlock()
if rtt == 0 {
return fmt.Errorf("RTT not tracked")
}
return nil
})
return rtt
}
prevA := checkRTT(t, sa)
prevB := checkRTT(t, sb)
// Wait to see if RTT is updated
checkUpdated := func(t *testing.T, s *Server, prev time.Duration) {
attempts := 0
timeout := time.Now().Add(2 * firstPingInterval)
for time.Now().Before(timeout) {
if rtt := checkRTT(t, s); rtt != prev {
return
}
attempts++
if attempts == 5 {
s.mu.Lock()
for _, ln := range s.leafs {
ln.mu.Lock()
ln.rtt = 0
ln.mu.Unlock()
break
}
s.mu.Unlock()
}
time.Sleep(15 * time.Millisecond)
}
t.Fatalf("RTT probably not updated")
}
checkUpdated(t, sa, prevA)
checkUpdated(t, sb, prevB)
sa.Shutdown()
sb.Shutdown()
// Now check that initial RTT is computed prior to first PingInterval
// Get new options to avoid possible race changing the ping interval.
ob = DefaultOptions()
ob.Cluster.Name = "xyz"
ob.PingInterval = time.Minute
ob.LeafNode.Host = "127.0.0.1"
ob.LeafNode.Port = -1
sb = RunServer(ob)
defer sb.Shutdown()
lnBURL, _ = url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
oa = DefaultOptions()
oa.PingInterval = time.Minute
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{lnBURL}}}
sa = RunServer(oa)
defer sa.Shutdown()
checkLeafNodeConnected(t, sa)
checkRTT(t, sa)
checkRTT(t, sb)
}
func TestLeafNodeValidateAuthOptions(t *testing.T) {
opts := DefaultOptions()
opts.LeafNode.Username = "user1"
opts.LeafNode.Password = "pwd"
opts.LeafNode.Users = []*User{{Username: "user", Password: "pwd"}}
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(),
"can not have a single user/pass and a users array") {
t.Fatalf("Expected error about mixing single/multi users, got %v", err)
}
// Check duplicate user names
opts.LeafNode.Username = _EMPTY_
opts.LeafNode.Password = _EMPTY_
opts.LeafNode.Users = append(opts.LeafNode.Users, &User{Username: "user", Password: "pwd"})
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "duplicate user") {
t.Fatalf("Expected error about duplicate user, got %v", err)
}
}
func TestLeafNodeBasicAuthSingleton(t *testing.T) {
opts := DefaultOptions()
opts.LeafNode.Port = -1
opts.LeafNode.Account = "unknown"
if s, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "cannot find") {
if s != nil {
s.Shutdown()
}
t.Fatalf("Expected error about account not found, got %v", err)
}
template := `
port: -1
accounts: {
ACC1: { users = [{user: "user1", password: "user1"}] }
ACC2: { users = [{user: "user2", password: "user2"}] }
}
leafnodes: {
port: -1
authorization {
%s
account: "ACC1"
}
}
`
for iter, test := range []struct {
name string
userSpec string
lnURLCreds string
shouldFail bool
}{
{"no user creds required and no user so binds to ACC1", "", "", false},
{"no user creds required and pick user2 associated to ACC2", "", "user2:user2@", false},
{"no user creds required and unknown user should fail", "", "unknown:user@", true},
{"user creds required so binds to ACC1", "user: \"ln\"\npass: \"pwd\"", "ln:pwd@", false},
} {
t.Run(test.name, func(t *testing.T) {
conf := createConfFile(t, []byte(fmt.Sprintf(template, test.userSpec)))
s1, o1 := RunServerWithConfig(conf)
defer s1.Shutdown()
// Create a sub on "foo" for account ACC1 (user user1), which is the one
// bound to the accepted LN connection.
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
defer ncACC1.Close()
sub1 := natsSubSync(t, ncACC1, "foo")
natsFlush(t, ncACC1)
// Create a sub on "foo" for account ACC2 (user user2). This one should
// not receive any message.
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
defer ncACC2.Close()
sub2 := natsSubSync(t, ncACC2, "foo")
natsFlush(t, ncACC2)
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes: {
remotes = [ { url: "nats-leaf://%s%s:%d" } ]
}
`, test.lnURLCreds, o1.LeafNode.Host, o1.LeafNode.Port)))
s2, _ := RunServerWithConfig(conf)
defer s2.Shutdown()
if test.shouldFail {
// Wait a bit and ensure that there is no leaf node connection
time.Sleep(100 * time.Millisecond)
checkFor(t, time.Second, 15*time.Millisecond, func() error {
if n := s1.NumLeafNodes(); n != 0 {
return fmt.Errorf("Expected no leafnode connection, got %v", n)
}
return nil
})
return
}
checkLeafNodeConnected(t, s2)
nc := natsConnect(t, s2.ClientURL())
defer nc.Close()
natsPub(t, nc, "foo", []byte("hello"))
// If url contains known user, even when there is no credentials
// required, the connection will be bound to the user's account.
if iter == 1 {
// Should not receive on "ACC1", but should on "ACC2"
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Expected timeout error, got %v", err)
}
natsNexMsg(t, sub2, time.Second)
} else {
// Should receive on "ACC1"...
natsNexMsg(t, sub1, time.Second)
// but not received on "ACC2" since leafnode bound to account "ACC1".
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Expected timeout error, got %v", err)
}
}
})
}
}
func TestLeafNodeBasicAuthMultiple(t *testing.T) {
conf := createConfFile(t, []byte(`
port: -1
accounts: {
S1ACC1: { users = [{user: "user1", password: "user1"}] }
S1ACC2: { users = [{user: "user2", password: "user2"}] }
}
leafnodes: {
port: -1
authorization {
users = [
{user: "ln1", password: "ln1", account: "S1ACC1"}
{user: "ln2", password: "ln2", account: "S1ACC2"}
{user: "ln3", password: "ln3"}
]
}
}
`))
s1, o1 := RunServerWithConfig(conf)
defer s1.Shutdown()
// Make sure that we reject a LN connection if user does not match
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes: {
remotes = [{url: "nats-leaf://wron:user@%s:%d"}]
}
`, o1.LeafNode.Host, o1.LeafNode.Port)))
s2, _ := RunServerWithConfig(conf)
defer s2.Shutdown()
// Give a chance for s2 to attempt to connect and make sure that s1
// did not register a LN connection.
time.Sleep(100 * time.Millisecond)
if n := s1.NumLeafNodes(); n != 0 {
t.Fatalf("Expected no leafnode connection, got %v", n)
}
s2.Shutdown()
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
defer ncACC1.Close()
sub1 := natsSubSync(t, ncACC1, "foo")
natsFlush(t, ncACC1)
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
defer ncACC2.Close()
sub2 := natsSubSync(t, ncACC2, "foo")
natsFlush(t, ncACC2)
// We will start s2 with 2 LN connections that should bind local account S2ACC1
// to account S1ACC1 and S2ACC2 to account S1ACC2 on s1.
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
accounts {
S2ACC1 { users = [{user: "user1", password: "user1"}] }
S2ACC2 { users = [{user: "user2", password: "user2"}] }
}
leafnodes: {
remotes = [
{
url: "nats-leaf://ln1:ln1@%s:%d"
account: "S2ACC1"
}
{
url: "nats-leaf://ln2:ln2@%s:%d"
account: "S2ACC2"
}
]
}
`, o1.LeafNode.Host, o1.LeafNode.Port, o1.LeafNode.Host, o1.LeafNode.Port)))
s2, o2 := RunServerWithConfig(conf)
defer s2.Shutdown()
checkFor(t, 5*time.Second, 100*time.Millisecond, func() error {
if nln := s2.NumLeafNodes(); nln != 2 {
return fmt.Errorf("Expected 2 connected leafnodes for server %q, got %d", s2.ID(), nln)
}
return nil
})
// Create a user connection on s2 that binds to S2ACC1 (use user1).
nc1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o2.Host, o2.Port))
defer nc1.Close()
// Create an user connection on s2 that binds to S2ACC2 (use user2).
nc2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o2.Host, o2.Port))
defer nc2.Close()
// Now if a message is published from nc1, sub1 should receive it since
// their account are bound together.
natsPub(t, nc1, "foo", []byte("hello"))
natsNexMsg(t, sub1, time.Second)
// But sub2 should not receive it since different account.
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Expected timeout error, got %v", err)
}
// Now use nc2 (S2ACC2) to publish
natsPub(t, nc2, "foo", []byte("hello"))
// Expect sub2 to receive and sub1 not to.
natsNexMsg(t, sub2, time.Second)
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Expected timeout error, got %v", err)
}
// Now check that we don't panic if no account is specified for
// a given user.
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes: {
remotes = [
{ url: "nats-leaf://ln3:ln3@%s:%d" }
]
}
`, o1.LeafNode.Host, o1.LeafNode.Port)))
s3, _ := RunServerWithConfig(conf)
defer s3.Shutdown()
}
type loopDetectedLogger struct {
DummyLogger
ch chan string
}
func (l *loopDetectedLogger) Errorf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
if strings.Contains(msg, "Loop") {
select {
case l.ch <- msg:
default:
}
}
}
func TestLeafNodeLoop(t *testing.T) {
test := func(t *testing.T, cluster bool) {
// This test requires that we set the port to known value because
// we want A point to B and B to A.
oa := DefaultOptions()
if !cluster {
oa.Cluster.Port = 0
oa.Cluster.Name = _EMPTY_
}
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
oa.LeafNode.Port = 1234
ub, _ := url.Parse("nats://127.0.0.1:5678")
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ub}}}
oa.LeafNode.connDelay = 50 * time.Millisecond
sa := RunServer(oa)
defer sa.Shutdown()
l := &loopDetectedLogger{ch: make(chan string, 1)}
sa.SetLogger(l, false, false)
ob := DefaultOptions()
if !cluster {
ob.Cluster.Port = 0
ob.Cluster.Name = _EMPTY_
} else {
ob.Cluster.Name = "xyz"
}
ob.LeafNode.ReconnectInterval = 10 * time.Millisecond
ob.LeafNode.Port = 5678
ua, _ := url.Parse("nats://127.0.0.1:1234")
ob.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}}
ob.LeafNode.connDelay = 50 * time.Millisecond
sb := RunServer(ob)
defer sb.Shutdown()
select {
case <-l.ch:
// OK!
case <-time.After(2 * time.Second):
t.Fatalf("Did not get any error regarding loop")
}
sb.Shutdown()
ob.Port = -1
ob.Cluster.Port = -1
ob.LeafNode.Remotes = nil
sb = RunServer(ob)
defer sb.Shutdown()
checkLeafNodeConnected(t, sa)
}
t.Run("standalone", func(t *testing.T) { test(t, false) })
t.Run("cluster", func(t *testing.T) { test(t, true) })
}
func TestLeafNodeLoopFromDAG(t *testing.T) {
// We want B & C to point to A, A itself does not point to any other server.
// We need to cancel clustering since now this will suppress on its own.
oa := DefaultOptions()
oa.ServerName = "A"
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
oa.LeafNode.Port = -1
oa.Cluster = ClusterOpts{}
sa := RunServer(oa)
defer sa.Shutdown()
ua, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oa.LeafNode.Port))
// B will point to A
ob := DefaultOptions()
ob.ServerName = "B"
ob.LeafNode.ReconnectInterval = 10 * time.Millisecond
ob.LeafNode.Port = -1
ob.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}}
ob.Cluster = ClusterOpts{}
sb := RunServer(ob)
defer sb.Shutdown()
ub, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
checkLeafNodeConnected(t, sa)
checkLeafNodeConnected(t, sb)
// C will point to A and B
oc := DefaultOptions()
oc.ServerName = "C"
oc.LeafNode.ReconnectInterval = 10 * time.Millisecond
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}, {URLs: []*url.URL{ub}}}
oc.LeafNode.connDelay = 100 * time.Millisecond // Allow logger to be attached before connecting.
oc.Cluster = ClusterOpts{}
sc := RunServer(oc)
lc := &loopDetectedLogger{ch: make(chan string, 1)}
sc.SetLogger(lc, false, false)
// We should get an error.
select {
case <-lc.ch:
// OK
case <-time.After(2 * time.Second):
t.Fatalf("Did not get any error regarding loop")
}
// C should not be connected to anything.
checkLeafNodeConnectedCount(t, sc, 0)
// A and B are connected to each other.
checkLeafNodeConnectedCount(t, sa, 1)
checkLeafNodeConnectedCount(t, sb, 1)
// Shutdown C and restart without the loop.
sc.Shutdown()
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ub}}}
sc = RunServer(oc)
defer sc.Shutdown()
checkLeafNodeConnectedCount(t, sa, 1)
checkLeafNodeConnectedCount(t, sb, 2)
checkLeafNodeConnectedCount(t, sc, 1)
}
func TestLeafNodeCloseTLSConnection(t *testing.T) {
opts := DefaultOptions()
opts.DisableShortFirstPing = true
opts.LeafNode.Host = "127.0.0.1"
opts.LeafNode.Port = -1
opts.LeafNode.TLSTimeout = 100
tc := &TLSConfigOpts{
CertFile: "./configs/certs/server.pem",
KeyFile: "./configs/certs/key.pem",
Insecure: true,
}
tlsConf, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating tls config: %v", err)
}
opts.LeafNode.TLSConfig = tlsConf
opts.NoLog = true
opts.NoSigs = true
s := RunServer(opts)
defer s.Shutdown()
endpoint := fmt.Sprintf("%s:%d", opts.LeafNode.Host, opts.LeafNode.Port)
conn, err := net.DialTimeout("tcp", endpoint, 2*time.Second)
if err != nil {
t.Fatalf("Unexpected error on dial: %v", err)
}
defer conn.Close()
br := bufio.NewReaderSize(conn, 100)
if _, err := br.ReadString('\n'); err != nil {
t.Fatalf("Unexpected error reading INFO: %v", err)
}
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
defer tlsConn.Close()
if err := tlsConn.Handshake(); err != nil {
t.Fatalf("Unexpected error during handshake: %v", err)
}
connectOp := []byte("CONNECT {\"name\":\"leaf\",\"verbose\":false,\"pedantic\":false}\r\n")
if _, err := tlsConn.Write(connectOp); err != nil {
t.Fatalf("Unexpected error writing CONNECT: %v", err)
}
if _, err := tlsConn.Write([]byte("PING\r\n")); err != nil {
t.Fatalf("Unexpected error writing PING: %v", err)
}
checkLeafNodeConnected(t, s)
// Get leaf connection
var leaf *client
s.mu.Lock()
for _, l := range s.leafs {
leaf = l
break
}
s.mu.Unlock()
// Fill the buffer. We want to timeout on write so that nc.Close()
// would block due to a write that cannot complete.
buf := make([]byte, 64*1024)
done := false
for !done {
leaf.nc.SetWriteDeadline(time.Now().Add(time.Second))
if _, err := leaf.nc.Write(buf); err != nil {
done = true
}
leaf.nc.SetWriteDeadline(time.Time{})
}
ch := make(chan bool)
go func() {
select {
case <-ch:
return
case <-time.After(3 * time.Second):
fmt.Println("!!!! closeConnection is blocked, test will hang !!!")
return
}
}()
// Close the route
leaf.closeConnection(SlowConsumerWriteDeadline)
ch <- true
}
func TestLeafNodeTLSSaveName(t *testing.T) {
opts := DefaultOptions()
opts.LeafNode.Host = "127.0.0.1"
opts.LeafNode.Port = -1
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-noip.pem",
KeyFile: "../test/configs/certs/server-key-noip.pem",
Insecure: true,
}
tlsConf, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating tls config: %v", err)
}
opts.LeafNode.TLSConfig = tlsConf
s := RunServer(opts)
defer s.Shutdown()
lo := DefaultOptions()
u, _ := url.Parse(fmt.Sprintf("nats://localhost:%d", opts.LeafNode.Port))
lo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
lo.LeafNode.ReconnectInterval = 15 * time.Millisecond
ln := RunServer(lo)
defer ln.Shutdown()
// We know connection will fail, but it should not fail because of error such as:
// "cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs"
// This would mean that we are not saving the hostname to use during the TLS handshake.
le := &captureErrorLogger{errCh: make(chan string, 100)}
ln.SetLogger(le, false, false)
tm := time.NewTimer(time.Second)
var done bool
for !done {
select {
case err := <-le.errCh:
if strings.Contains(err, "doesn't contain any IP SANs") {
t.Fatalf("Got this error: %q", err)
}
case <-tm.C:
done = true
}
}
}
func TestLeafNodeRemoteWrongPort(t *testing.T) {
for _, test1 := range []struct {
name string
clusterAdvertise bool
leafnodeAdvertise bool
}{
{"advertise_on", false, false},
{"cluster_no_advertise", true, false},
{"leafnode_no_advertise", false, true},
} {
t.Run(test1.name, func(t *testing.T) {
oa := DefaultOptions()
// Make sure we have all ports (client, route, gateway) and we will try
// to create a leafnode to connection to each and make sure we get the error.
oa.Cluster.NoAdvertise = test1.clusterAdvertise
oa.Cluster.Name = "A"
oa.Cluster.Host = "127.0.0.1"
oa.Cluster.Port = -1
oa.Gateway.Host = "127.0.0.1"
oa.Gateway.Port = -1
oa.Gateway.Name = "A"
oa.LeafNode.Host = "127.0.0.1"
oa.LeafNode.Port = -1
oa.LeafNode.NoAdvertise = test1.leafnodeAdvertise
oa.Accounts = []*Account{NewAccount("sys")}
oa.SystemAccount = "sys"
sa := RunServer(oa)
defer sa.Shutdown()
ob := DefaultOptions()
ob.Cluster.NoAdvertise = test1.clusterAdvertise
ob.Cluster.Name = "A"
ob.Cluster.Host = "127.0.0.1"
ob.Cluster.Port = -1
ob.Routes = RoutesFromStr(fmt.Sprintf("nats://%s:%d", oa.Cluster.Host, oa.Cluster.Port))
ob.Gateway.Host = "127.0.0.1"
ob.Gateway.Port = -1
ob.Gateway.Name = "A"
ob.LeafNode.Host = "127.0.0.1"
ob.LeafNode.Port = -1
ob.LeafNode.NoAdvertise = test1.leafnodeAdvertise
ob.Accounts = []*Account{NewAccount("sys")}
ob.SystemAccount = "sys"
sb := RunServer(ob)
defer sb.Shutdown()
checkClusterFormed(t, sa, sb)
for _, test := range []struct {
name string
port int
}{
{"client", oa.Port},
{"cluster", oa.Cluster.Port},
{"gateway", oa.Gateway.Port},
} {
t.Run(test.name, func(t *testing.T) {
oc := DefaultOptions()
// Server with the wrong config against non leafnode port.
leafURL, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", test.port))
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{leafURL}}}
oc.LeafNode.ReconnectInterval = 5 * time.Millisecond
sc := RunServer(oc)
defer sc.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 10)}
sc.SetLogger(l, true, true)
select {
case e := <-l.errCh:
if strings.Contains(e, ErrConnectedToWrongPort.Error()) {
return
}
case <-time.After(2 * time.Second):
t.Fatalf("Did not get any error about connecting to wrong port for %q - %q",
test1.name, test.name)
}
})
}
})
}
}
func TestLeafNodeRemoteIsHub(t *testing.T) {
oa := testDefaultOptionsForGateway("A")
oa.Accounts = []*Account{NewAccount("sys")}
oa.SystemAccount = "sys"
sa := RunServer(oa)
defer sa.Shutdown()
lno := DefaultOptions()
lno.LeafNode.Host = "127.0.0.1"
lno.LeafNode.Port = -1
ln := RunServer(lno)
defer ln.Shutdown()
ob1 := testGatewayOptionsFromToWithServers(t, "B", "A", sa)
ob1.Accounts = []*Account{NewAccount("sys")}
ob1.SystemAccount = "sys"
ob1.Cluster.Host = "127.0.0.1"
ob1.Cluster.Port = -1
ob1.LeafNode.Host = "127.0.0.1"
ob1.LeafNode.Port = -1
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lno.LeafNode.Port))
ob1.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
Hub: true,
},
}
sb1 := RunServer(ob1)
defer sb1.Shutdown()
waitForOutboundGateways(t, sb1, 1, 2*time.Second)
waitForInboundGateways(t, sb1, 1, 2*time.Second)
waitForOutboundGateways(t, sa, 1, 2*time.Second)
waitForInboundGateways(t, sa, 1, 2*time.Second)
checkLeafNodeConnected(t, sb1)
// For now, due to issue 977, let's restart the leafnode so that the
// leafnode connect is propagated in the super-cluster.
ln.Shutdown()
ln = RunServer(lno)
defer ln.Shutdown()
checkLeafNodeConnected(t, sb1)
// Connect another server in cluster B
ob2 := testGatewayOptionsFromToWithServers(t, "B", "A", sa)
ob2.Accounts = []*Account{NewAccount("sys")}
ob2.SystemAccount = "sys"
ob2.Cluster.Host = "127.0.0.1"
ob2.Cluster.Port = -1
ob2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", ob1.Cluster.Port))
sb2 := RunServer(ob2)
defer sb2.Shutdown()
checkClusterFormed(t, sb1, sb2)
waitForOutboundGateways(t, sb2, 1, 2*time.Second)
expectedSubs := ln.NumSubscriptions() + 2
// Create sub on "foo" connected to sa
ncA := natsConnect(t, sa.ClientURL())
defer ncA.Close()
subFoo := natsSubSync(t, ncA, "foo")
// Create sub on "bar" connected to sb2
ncB2 := natsConnect(t, sb2.ClientURL())
defer ncB2.Close()
subBar := natsSubSync(t, ncB2, "bar")
// Make sure subscriptions have propagated to the leafnode.
checkFor(t, time.Second, 10*time.Millisecond, func() error {
if subs := ln.NumSubscriptions(); subs < expectedSubs {
return fmt.Errorf("Number of subs is %d", subs)
}
return nil
})
// Create pub connection on leafnode
ncLN := natsConnect(t, ln.ClientURL())
defer ncLN.Close()
// Publish on foo and make sure it is received.
natsPub(t, ncLN, "foo", []byte("msg"))
natsNexMsg(t, subFoo, time.Second)
// Publish on foo and make sure it is received.
natsPub(t, ncLN, "bar", []byte("msg"))
natsNexMsg(t, subBar, time.Second)
}
func TestLeafNodePermissions(t *testing.T) {
lo1 := DefaultOptions()
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
ln1 := RunServer(lo1)
defer ln1.Shutdown()
errLog := &captureErrorLogger{errCh: make(chan string, 1)}
ln1.SetLogger(errLog, false, false)
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
lo2 := DefaultOptions()
lo2.Cluster.Name = "xyz"
lo2.LeafNode.ReconnectInterval = 5 * time.Millisecond
lo2.LeafNode.connDelay = 100 * time.Millisecond
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
DenyExports: []string{"export.*", "export"},
DenyImports: []string{"import.*", "import"},
},
}
ln2 := RunServer(lo2)
defer ln2.Shutdown()
checkLeafNodeConnected(t, ln1)
// Create clients on ln1 and ln2
nc1, err := nats.Connect(ln1.ClientURL())
if err != nil {
t.Fatalf("Error creating client: %v", err)
}
defer nc1.Close()
nc2, err := nats.Connect(ln2.ClientURL())
if err != nil {
t.Fatalf("Error creating client: %v", err)
}
defer nc2.Close()
checkSubs := func(acc *Account, expected int) {
t.Helper()
checkFor(t, time.Second, 15*time.Millisecond, func() error {
if n := acc.TotalSubs(); n != expected {
return fmt.Errorf("Expected %d subs, got %v", expected, n)
}
return nil
})
}
// Create a sub on ">" on LN1
subAll := natsSubSync(t, nc1, ">")
// this should be registered in LN2 (there is 1 sub for LN1 $LDS subject) + SYS IMPORTS
checkSubs(ln2.globalAccount(), 12)
// Check deny export clause from messages published from LN2
for _, test := range []struct {
name string
subject string
received bool
}{
{"do not send on export.bat", "export.bat", false},
{"do not send on export", "export", false},
{"send on foo", "foo", true},
{"send on export.this.one", "export.this.one", true},
} {
t.Run(test.name, func(t *testing.T) {
nc2.Publish(test.subject, []byte("msg"))
if test.received {
natsNexMsg(t, subAll, time.Second)
} else {
if _, err := subAll.NextMsg(50 * time.Millisecond); err == nil {
t.Fatalf("Should not have received message on %q", test.subject)
}
}
})
}
subAll.Unsubscribe()
// Goes down by 1.
checkSubs(ln2.globalAccount(), 11)
// We used to make sure we would not do subscriptions however that
// was incorrect. We need to check publishes, not the subscriptions.
// For instance if we can publish across a leafnode to foo, and the
// other side has a subsxcription for '*' the message should cross
// the leafnode. The old way would not allow this.
// Now check deny import clause.
// As of now, we don't suppress forwarding of subscriptions on LN2 that
// match the deny import clause to be forwarded to LN1. However, messages
// should still not be able to come back to LN2.
for _, test := range []struct {
name string
subSubject string
pubSubject string
ok bool
}{
{"reject import on import.*", "import.*", "import.bad", false},
{"reject import on import", "import", "import", false},
{"accepts import on foo", "foo", "foo", true},
{"accepts import on import.this.one.ok", "import.*.>", "import.this.one.ok", true},
} {
t.Run(test.name, func(t *testing.T) {
sub := natsSubSync(t, nc2, test.subSubject)
checkSubs(ln2.globalAccount(), 12)
if !test.ok {
nc1.Publish(test.pubSubject, []byte("msg"))
if _, err := sub.NextMsg(50 * time.Millisecond); err == nil {
t.Fatalf("Did not expect to get the message")
}
} else {
checkSubs(ln1.globalAccount(), 11)
nc1.Publish(test.pubSubject, []byte("msg"))
natsNexMsg(t, sub, time.Second)
}
sub.Unsubscribe()
checkSubs(ln1.globalAccount(), 10)
})
}
}
func TestLeafNodePermissionsConcurrentAccess(t *testing.T) {
lo1 := DefaultOptions()
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
ln1 := RunServer(lo1)
defer ln1.Shutdown()
nc1 := natsConnect(t, ln1.ClientURL())
defer nc1.Close()
natsSub(t, nc1, "_INBOX.>", func(_ *nats.Msg) {})
natsFlush(t, nc1)
ch := make(chan struct{}, 1)
wg := sync.WaitGroup{}
wg.Add(2)
publish := func(nc *nats.Conn) {
defer wg.Done()
for {
select {
case <-ch:
return
default:
nc.Publish(nats.NewInbox(), []byte("hello"))
}
}
}
go publish(nc1)
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
lo2 := DefaultOptions()
lo2.Cluster.Name = "xyz"
lo2.LeafNode.ReconnectInterval = 5 * time.Millisecond
lo2.LeafNode.connDelay = 500 * time.Millisecond
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
DenyExports: []string{"foo"},
DenyImports: []string{"bar"},
},
}
ln2 := RunServer(lo2)
defer ln2.Shutdown()
nc2 := natsConnect(t, ln2.ClientURL())
defer nc2.Close()
natsSub(t, nc2, "_INBOX.>", func(_ *nats.Msg) {})
natsFlush(t, nc2)
go publish(nc2)
checkLeafNodeConnected(t, ln1)
checkLeafNodeConnected(t, ln2)
time.Sleep(50 * time.Millisecond)
close(ch)
wg.Wait()
}
func TestLeafNodePubAllowedPruning(t *testing.T) {
c := &client{}
c.setPermissions(&Permissions{Publish: &SubjectPermission{Allow: []string{"foo"}}})
gr := 100
wg := sync.WaitGroup{}
wg.Add(gr)
for i := 0; i < gr; i++ {
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
c.pubAllowed(nats.NewInbox())
}
}()
}
wg.Wait()
if n := int(atomic.LoadInt32(&c.perms.pcsz)); n > maxPermCacheSize {
t.Fatalf("Expected size to be less than %v, got %v", maxPermCacheSize, n)
}
if n := atomic.LoadInt32(&c.perms.prun); n != 0 {
t.Fatalf("c.perms.prun should be 0, was %v", n)
}
}
func TestLeafNodeExportPermissionsNotForSpecialSubs(t *testing.T) {
lo1 := DefaultOptions()
lo1.Accounts = []*Account{NewAccount("SYS")}
lo1.SystemAccount = "SYS"
lo1.Cluster.Name = "A"
lo1.Gateway.Name = "A"
lo1.Gateway.Port = -1
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
ln1 := RunServer(lo1)
defer ln1.Shutdown()
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
lo2 := DefaultOptions()
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
DenyExports: []string{">"},
},
}
ln2 := RunServer(lo2)
defer ln2.Shutdown()
checkLeafNodeConnected(t, ln1)
// The deny is totally restrictive, but make sure that we still accept the $LDS, $GR and _GR_ go from LN1.
checkFor(t, time.Second, 15*time.Millisecond, func() error {
// We should have registered the 3 subs from the accepting leafnode.
if n := ln2.globalAccount().TotalSubs(); n != 9 {
return fmt.Errorf("Expected %d subs, got %v", 9, n)
}
return nil
})
}
// Make sure that if the node that detects the loop (and sends the error and
// close the connection) is the accept side, the remote node (the one that solicits)
// properly use the reconnect delay.
func TestLeafNodeLoopDetectedOnAcceptSide(t *testing.T) {
bo := DefaultOptions()
bo.LeafNode.Host = "127.0.0.1"
bo.LeafNode.Port = -1
b := RunServer(bo)
defer b.Shutdown()
l := &loopDetectedLogger{ch: make(chan string, 1)}
b.SetLogger(l, false, false)
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", bo.LeafNode.Port))
ao := testDefaultOptionsForGateway("A")
ao.Accounts = []*Account{NewAccount("SYS")}
ao.SystemAccount = "SYS"
ao.LeafNode.ReconnectInterval = 5 * time.Millisecond
ao.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
Hub: true,
},
}
a := RunServer(ao)
defer a.Shutdown()
co := testGatewayOptionsFromToWithServers(t, "C", "A", a)
co.Accounts = []*Account{NewAccount("SYS")}
co.SystemAccount = "SYS"
co.LeafNode.ReconnectInterval = 5 * time.Millisecond
co.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{u},
Hub: true,
},
}
c := RunServer(co)
defer c.Shutdown()
for i := 0; i < 2; i++ {
select {
case <-l.ch:
// OK
case <-time.After(200 * time.Millisecond):
// We are likely to detect from each A and C servers,
// but consider a failure if we did not receive any.
if i == 0 {
t.Fatalf("Should have detected loop")
}
}
}
// The reconnect attempt is set to 5ms, but the default loop delay
// is 30 seconds, so we should not get any new error for that long.
// Check if we are getting more errors..
select {
case e := <-l.ch:
t.Fatalf("Should not have gotten another error, got %q", e)
case <-time.After(50 * time.Millisecond):
// OK!
}
}
func TestLeafNodeHubWithGateways(t *testing.T) {
ao := DefaultOptions()
ao.ServerName = "A"
ao.LeafNode.Host = "127.0.0.1"
ao.LeafNode.Port = -1
a := RunServer(ao)
defer a.Shutdown()
ua, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ao.LeafNode.Port))
bo := testDefaultOptionsForGateway("B")
bo.ServerName = "B"
bo.Accounts = []*Account{NewAccount("SYS")}
bo.SystemAccount = "SYS"
bo.LeafNode.ReconnectInterval = 5 * time.Millisecond
bo.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{ua},
Hub: true,
},
}
b := RunServer(bo)
defer b.Shutdown()
do := DefaultOptions()
do.ServerName = "D"
do.LeafNode.Host = "127.0.0.1"
do.LeafNode.Port = -1
d := RunServer(do)
defer d.Shutdown()
ud, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", do.LeafNode.Port))
co := testGatewayOptionsFromToWithServers(t, "C", "B", b)
co.ServerName = "C"
co.Accounts = []*Account{NewAccount("SYS")}
co.SystemAccount = "SYS"
co.LeafNode.ReconnectInterval = 5 * time.Millisecond
co.LeafNode.Remotes = []*RemoteLeafOpts{
{
URLs: []*url.URL{ud},
Hub: true,
},
}
c := RunServer(co)
defer c.Shutdown()
waitForInboundGateways(t, b, 1, 2*time.Second)
waitForInboundGateways(t, c, 1, 2*time.Second)
checkLeafNodeConnected(t, a)
checkLeafNodeConnected(t, d)
// Create a responder on D
ncD := natsConnect(t, d.ClientURL())
defer ncD.Close()
ncD.Subscribe("service", func(m *nats.Msg) {
m.Respond([]byte("reply"))
})
ncD.Flush()
checkFor(t, time.Second, 15*time.Millisecond, func() error {
acc := a.globalAccount()
if r := acc.sl.Match("service"); r != nil && len(r.psubs) == 1 {
return nil
}
return fmt.Errorf("subscription still not registered")
})
// Create requestor on A and send the request, expect a reply.
ncA := natsConnect(t, a.ClientURL())
defer ncA.Close()
if msg, err := ncA.Request("service", []byte("request"), time.Second); err != nil {
t.Fatalf("Failed to get reply: %v", err)
} else if string(msg.Data) != "reply" {
t.Fatalf("Unexpected reply: %q", msg.Data)
}
}
func TestLeafNodeTmpClients(t *testing.T) {
ao := DefaultOptions()
ao.LeafNode.Host = "127.0.0.1"
ao.LeafNode.Port = -1
a := RunServer(ao)
defer a.Shutdown()
c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", ao.LeafNode.Port))
if err != nil {
t.Fatalf("Error connecting: %v", err)
}
defer c.Close()
// Read info
br := bufio.NewReader(c)
br.ReadLine()
checkTmp := func(expected int) {
t.Helper()
checkFor(t, time.Second, 15*time.Millisecond, func() error {
a.grMu.Lock()
l := len(a.grTmpClients)
a.grMu.Unlock()
if l != expected {
return fmt.Errorf("Expected tmp map to have %v entries, got %v", expected, l)
}
return nil
})
}
checkTmp(1)
// Close client and wait check that it is removed.
c.Close()
checkTmp(0)
// Check with normal leafnode connection that once connected,
// the tmp map is also emptied.
bo := DefaultOptions()
bo.Cluster.Name = "xyz"
bo.LeafNode.ReconnectInterval = 5 * time.Millisecond
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ao.LeafNode.Port))
if err != nil {
t.Fatalf("Error creating url: %v", err)
}
bo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
b := RunServer(bo)
defer b.Shutdown()
checkLeafNodeConnected(t, b)
checkTmp(0)
}
func TestLeafNodeTLSVerifyAndMap(t *testing.T) {
accName := "MyAccount"
acc := NewAccount(accName)
certUserName := "CN=example.com,OU=NATS.io"
users := []*User{{Username: certUserName, Account: acc}}
for _, test := range []struct {
name string
leafUsers bool
provideCert bool
}{
{"no users override, provides cert", false, true},
{"no users override, does not provide cert", false, false},
{"users override, provides cert", true, true},
{"users override, does not provide cert", true, false},
} {
t.Run(test.name, func(t *testing.T) {
o := DefaultOptions()
o.Accounts = []*Account{acc}
o.LeafNode.Host = "127.0.0.1"
o.LeafNode.Port = -1
if test.leafUsers {
o.LeafNode.Users = users
} else {
o.Users = users
}
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/tlsauth/server.pem",
KeyFile: "../test/configs/certs/tlsauth/server-key.pem",
CaFile: "../test/configs/certs/tlsauth/ca.pem",
Verify: true,
}
tlsc, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error creating tls config: %v", err)
}
o.LeafNode.TLSConfig = tlsc
o.LeafNode.TLSMap = true
s := RunServer(o)
defer s.Shutdown()
slo := DefaultOptions()
slo.Cluster.Name = "xyz"
sltlsc := &tls.Config{}
if test.provideCert {
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/tlsauth/client.pem",
KeyFile: "../test/configs/certs/tlsauth/client-key.pem",
}
var err error
sltlsc, err = GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating tls config: %v", err)
}
}
sltlsc.InsecureSkipVerify = true
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", o.LeafNode.Host, o.LeafNode.Port))
slo.LeafNode.Remotes = []*RemoteLeafOpts{
{
TLSConfig: sltlsc,
URLs: []*url.URL{u},
},
}
sl := RunServer(slo)
defer sl.Shutdown()
if !test.provideCert {
// Wait a bit and make sure we are not connecting
time.Sleep(100 * time.Millisecond)
checkLeafNodeConnectedCount(t, s, 0)
return
}
checkLeafNodeConnected(t, s)
var uname string
var accname string
s.mu.Lock()
for _, c := range s.leafs {
c.mu.Lock()
uname = c.opts.Username
if c.acc != nil {
accname = c.acc.GetName()
}
c.mu.Unlock()
}
s.mu.Unlock()
if uname != certUserName {
t.Fatalf("Expected username %q, got %q", certUserName, uname)
}
if accname != accName {
t.Fatalf("Expected account %q, got %v", accName, accname)
}
})
}
}
type chanLogger struct {
DummyLogger
triggerChan chan string
}
func (l *chanLogger) Warnf(format string, v ...interface{}) {
l.triggerChan <- fmt.Sprintf(format, v...)
}
func (l *chanLogger) Errorf(format string, v ...interface{}) {
l.triggerChan <- fmt.Sprintf(format, v...)
}
const (
testLeafNodeTLSVerifyAndMapSrvA = `
listen: 127.0.0.1:-1
leaf {
listen: "127.0.0.1:-1"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
timeout: 2
verify_and_map: true
}
authorization {
users [{
user: "%s"
}]
}
}
`
testLeafNodeTLSVerifyAndMapSrvB = `
listen: -1
leaf {
remotes [
{
url: "tls://user-provided-in-url@localhost:%d"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
}
}
]
}`
)
func TestLeafNodeTLSVerifyAndMapCfgPass(t *testing.T) {
l := &chanLogger{triggerChan: make(chan string, 100)}
defer close(l.triggerChan)
confA := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvA, "localhost")))
srvA, optsA := RunServerWithConfig(confA)
defer srvA.Shutdown()
srvA.SetLogger(l, true, true)
confB := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvB, optsA.LeafNode.Port)))
ob := LoadConfig(confB)
ob.LeafNode.ReconnectInterval = 50 * time.Millisecond
srvB := RunServer(ob)
defer srvB.Shutdown()
// Now make sure that the leaf node connection is up and the correct account was picked
checkFor(t, 10*time.Second, 10*time.Millisecond, func() error {
for _, srv := range []*Server{srvA, srvB} {
if nln := srv.NumLeafNodes(); nln != 1 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
if leafz, err := srv.Leafz(nil); err != nil {
if len(leafz.Leafs) != 1 {
return fmt.Errorf("Number of leaf nodes returned by LEAFZ is not one: %d", len(leafz.Leafs))
} else if leafz.Leafs[0].Account != DEFAULT_GLOBAL_ACCOUNT {
return fmt.Errorf("Account used is not $G: %s", leafz.Leafs[0].Account)
}
}
}
return nil
})
// Make sure that the user name in the url was ignored and a warning printed
for {
select {
case w := <-l.triggerChan:
if w == `User "user-provided-in-url" found in connect proto, but user required from cert` {
return
}
case <-time.After(2 * time.Second):
t.Fatal("Did not get expected warning")
}
}
}
func TestLeafNodeTLSVerifyAndMapCfgFail(t *testing.T) {
l := &chanLogger{triggerChan: make(chan string, 100)}
defer close(l.triggerChan)
// use certificate with SAN localhost, but configure the server to not accept it
// instead provide a name matching the user (to be matched by failed
confA := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvA, "user-provided-in-url")))
srvA, optsA := RunServerWithConfig(confA)
defer srvA.Shutdown()
srvA.SetLogger(l, true, true)
confB := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvB, optsA.LeafNode.Port)))
ob := LoadConfig(confB)
ob.LeafNode.ReconnectInterval = 50 * time.Millisecond
srvB := RunServer(ob)
defer srvB.Shutdown()
// Now make sure that the leaf node connection is down
checkFor(t, 10*time.Second, 10*time.Millisecond, func() error {
for _, srv := range []*Server{srvA, srvB} {
if nln := srv.NumLeafNodes(); nln != 0 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
}
return nil
})
// Make sure that the connection was closed for the right reason
for {
select {
case w := <-l.triggerChan:
if strings.Contains(w, ErrAuthentication.Error()) {
return
}
case <-time.After(2 * time.Second):
t.Fatal("Did not get expected warning")
}
}
}
func TestLeafNodeOriginClusterInfo(t *testing.T) {
hopts := DefaultOptions()
hopts.ServerName = "hub"
hopts.LeafNode.Port = -1
hub := RunServer(hopts)
defer hub.Shutdown()
conf := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [ { url: "nats://127.0.0.1:%d" } ]
}
`, hopts.LeafNode.Port)))
opts, err := ProcessConfigFile(conf)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
opts.NoLog, opts.NoSigs = true, true
s := RunServer(opts)
defer s.Shutdown()
checkLeafNodeConnected(t, s)
// Check the info on the leadnode client in the hub.
grabLeaf := func() *client {
var l *client
hub.mu.Lock()
for _, l = range hub.leafs {
break
}
hub.mu.Unlock()
return l
}
l := grabLeaf()
if rc := l.remoteCluster(); rc != "" {
t.Fatalf("Expected an empty remote cluster, got %q", rc)
}
s.Shutdown()
// Now make our leafnode part of a cluster.
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leaf {
remotes [ { url: "nats://127.0.0.1:%d" } ]
}
cluster {
name: "xyz"
listen: "127.0.0.1:-1"
}
`, hopts.LeafNode.Port)))
opts, err = ProcessConfigFile(conf)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
opts.NoLog, opts.NoSigs = true, true
s = RunServer(opts)
defer s.Shutdown()
checkLeafNodeConnected(t, s)
l = grabLeaf()
if rc := l.remoteCluster(); rc != "xyz" {
t.Fatalf("Expected a remote cluster name of \"xyz\", got %q", rc)
}
pcid := l.cid
// Now make sure that if we update our cluster name, simulating the settling
// of dynamic cluster names between competing servers.
s.setClusterName("xyz")
// Make sure we disconnect and reconnect.
checkLeafNodeConnectedCount(t, s, 0)
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, hub)
l = grabLeaf()
if rc := l.remoteCluster(); rc != "xyz" {
t.Fatalf("Expected a remote cluster name of \"xyz\", got %q", rc)
}
// Make sure we reconnected and have a new CID.
if l.cid == pcid {
t.Fatalf("Expected a different id, got the same")
}
}
type proxyAcceptDetectFailureLate struct {
sync.Mutex
wg sync.WaitGroup
acceptPort int
l net.Listener
srvs []net.Conn
leaf net.Conn
startChan chan struct{}
}
func (p *proxyAcceptDetectFailureLate) run(t *testing.T) int {
return p.runEx(t, false)
}
func (p *proxyAcceptDetectFailureLate) runEx(t *testing.T, needStart bool) int {
l, err := natsListen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Error on listen: %v", err)
}
p.Lock()
var startChan chan struct{}
if needStart {
startChan = make(chan struct{})
p.startChan = startChan
}
p.l = l
p.Unlock()
port := l.Addr().(*net.TCPAddr).Port
p.wg.Add(1)
go func() {
defer p.wg.Done()
defer l.Close()
defer func() {
p.Lock()
for _, c := range p.srvs {
c.Close()
}
p.Unlock()
}()
if startChan != nil {
<-startChan
}
for {
c, err := l.Accept()
if err != nil {
return
}
srv, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", p.acceptPort))
if err != nil {
return
}
p.Lock()
p.leaf = c
p.srvs = append(p.srvs, srv)
p.Unlock()
transfer := func(c1, c2 net.Conn) {
var buf [1024]byte
for {
n, err := c1.Read(buf[:])
if err != nil {
return
}
if _, err := c2.Write(buf[:n]); err != nil {
return
}
}
}
go transfer(srv, c)
go transfer(c, srv)
}
}()
return port
}
func (p *proxyAcceptDetectFailureLate) start() {
p.Lock()
if p.startChan != nil {
close(p.startChan)
p.startChan = nil
}
p.Unlock()
}
func (p *proxyAcceptDetectFailureLate) close() {
p.Lock()
if p.startChan != nil {
close(p.startChan)
p.startChan = nil
}
p.l.Close()
p.Unlock()
p.wg.Wait()
}
type oldConnReplacedLogger struct {
DummyLogger
errCh chan string
warnCh chan string
}
func (l *oldConnReplacedLogger) Errorf(format string, v ...interface{}) {
select {
case l.errCh <- fmt.Sprintf(format, v...):
default:
}
}
func (l *oldConnReplacedLogger) Warnf(format string, v ...interface{}) {
select {
case l.warnCh <- fmt.Sprintf(format, v...):
default:
}
}
// This test will simulate that the accept side does not detect the connection
// has been closed early enough. The soliciting side will attempt to reconnect
// and we should not be getting the "loop detected" error.
func TestLeafNodeLoopDetectedDueToReconnect(t *testing.T) {
o := DefaultOptions()
o.LeafNode.Host = "127.0.0.1"
o.LeafNode.Port = -1
s := RunServer(o)
defer s.Shutdown()
l := &oldConnReplacedLogger{errCh: make(chan string, 10), warnCh: make(chan string, 10)}
s.SetLogger(l, false, false)
p := &proxyAcceptDetectFailureLate{acceptPort: o.LeafNode.Port}
defer p.close()
port := p.run(t)
aurl, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
ol := DefaultOptions()
ol.Cluster.Name = "cde"
ol.LeafNode.ReconnectInterval = 50 * time.Millisecond
ol.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{aurl}}}
sl := RunServer(ol)
defer sl.Shutdown()
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, sl)
// Cause disconnect client side...
p.Lock()
p.leaf.Close()
p.Unlock()
// Make sure we did not get the loop detected error
select {
case e := <-l.errCh:
if strings.Contains(e, "Loop detected") {
t.Fatalf("Loop detected: %v", e)
}
case <-time.After(250 * time.Millisecond):
// We are ok
}
// Now make sure we got the warning
select {
case w := <-l.warnCh:
if !strings.Contains(w, "Replacing connection from same server") {
t.Fatalf("Unexpected warning: %v", w)
}
case <-time.After(time.Second):
t.Fatal("Did not get expected warning")
}
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, sl)
}
func TestLeafNodeTwoRemotesBindToSameHubAccount(t *testing.T) {
opts := DefaultOptions()
opts.LeafNode.Host = "127.0.0.1"
opts.LeafNode.Port = -1
s := RunServer(opts)
defer s.Shutdown()
for _, test := range []struct {
name string
account string
fail bool
}{
{"different local accounts", "b", false},
{"same local accounts", "a", true},
} {
t.Run(test.name, func(t *testing.T) {
conf := `
listen: 127.0.0.1:-1
cluster { name: ln22, listen: 127.0.0.1:-1 }
accounts {
a { users [ {user: a, password: a} ]}
b { users [ {user: b, password: b} ]}
}
leafnodes {
remotes = [
{
url: nats-leaf://127.0.0.1:%[1]d
account: a
}
{
url: nats-leaf://127.0.0.1:%[1]d
account: %s
}
]
}
`
lconf := createConfFile(t, []byte(fmt.Sprintf(conf, opts.LeafNode.Port, test.account)))
lopts, err := ProcessConfigFile(lconf)
if err != nil {
t.Fatalf("Error loading config file: %v", err)
}
lopts.NoLog = false
ln, err := NewServer(lopts)
if err != nil {
t.Fatalf("Error creating server: %v", err)
}
defer ln.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 10)}
ln.SetLogger(l, false, false)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
ln.Start()
}()
select {
case err := <-l.errCh:
if test.fail && !strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
t.Fatalf("Did not get expected duplicate connection error: %v", err)
} else if !test.fail && strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
t.Fatalf("Incorrectly detected a duplicate connection: %v", err)
}
case <-time.After(250 * time.Millisecond):
if test.fail {
t.Fatal("Did not get expected error")
}
}
ln.Shutdown()
wg.Wait()
})
}
}
func TestLeafNodeNoDuplicateWithinCluster(t *testing.T) {
// This set the cluster name to "abc"
oSrv1 := DefaultOptions()
oSrv1.ServerName = "srv1"
oSrv1.LeafNode.Host = "127.0.0.1"
oSrv1.LeafNode.Port = -1
srv1 := RunServer(oSrv1)
defer srv1.Shutdown()
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
oLeaf1 := DefaultOptions()
oLeaf1.ServerName = "leaf1"
oLeaf1.Cluster.Name = "xyz"
oLeaf1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
leaf1 := RunServer(oLeaf1)
defer leaf1.Shutdown()
leaf1ClusterURL := fmt.Sprintf("nats://127.0.0.1:%d", oLeaf1.Cluster.Port)
oLeaf2 := DefaultOptions()
oLeaf2.ServerName = "leaf2"
oLeaf2.Cluster.Name = "xyz"
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
oLeaf2.Routes = RoutesFromStr(leaf1ClusterURL)
leaf2 := RunServer(oLeaf2)
defer leaf2.Shutdown()
checkClusterFormed(t, leaf1, leaf2)
checkLeafNodeConnectedCount(t, srv1, 2)
checkLeafNodeConnected(t, leaf1)
checkLeafNodeConnected(t, leaf2)
ncSrv1 := natsConnect(t, srv1.ClientURL())
defer ncSrv1.Close()
natsQueueSub(t, ncSrv1, "foo", "queue", func(m *nats.Msg) {
m.Respond([]byte("from srv1"))
})
ncLeaf1 := natsConnect(t, leaf1.ClientURL())
defer ncLeaf1.Close()
natsQueueSub(t, ncLeaf1, "foo", "queue", func(m *nats.Msg) {
m.Respond([]byte("from leaf1"))
})
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
defer ncLeaf2.Close()
// Check that "foo" interest is available everywhere.
// For this test, we want to make sure that the 2 queue subs are
// registered on all servers, so we don't use checkSubInterest
// which would simply return "true" if there is any interest on "foo".
servers := []*Server{srv1, leaf1, leaf2}
checkFor(t, time.Second, 15*time.Millisecond, func() error {
for _, s := range servers {
acc, err := s.LookupAccount(globalAccountName)
if err != nil {
return err
}
acc.mu.RLock()
r := acc.sl.Match("foo")
ok := len(r.qsubs) == 1 && len(r.qsubs[0]) == 2
acc.mu.RUnlock()
if !ok {
return fmt.Errorf("interest not propagated on %q", s.Name())
}
}
return nil
})
// Send requests (from leaf2). For this test to make sure that
// there is no duplicate, we want to make sure that we check for
// multiple replies and that the reply subject subscription has
// been propagated everywhere.
sub := natsSubSync(t, ncLeaf2, "reply_subj")
natsFlush(t, ncLeaf2)
// Here we have a single sub on "reply_subj" so using checkSubInterest is ok.
checkSubInterest(t, srv1, globalAccountName, "reply_subj", time.Second)
checkSubInterest(t, leaf1, globalAccountName, "reply_subj", time.Second)
checkSubInterest(t, leaf2, globalAccountName, "reply_subj", time.Second)
for i := 0; i < 5; i++ {
// Now send the request
natsPubReq(t, ncLeaf2, "foo", sub.Subject, []byte("req"))
// Check that we get the reply
replyMsg := natsNexMsg(t, sub, time.Second)
// But make sure we received only 1!
if otherReply, _ := sub.NextMsg(100 * time.Millisecond); otherReply != nil {
t.Fatalf("Received duplicate reply, first was %q, followed by %q",
replyMsg.Data, otherReply.Data)
}
// We also should have preferred the queue sub that is in the leaf cluster.
if string(replyMsg.Data) != "from leaf1" {
t.Fatalf("Expected reply from leaf1, got %q", replyMsg.Data)
}
}
}
func TestLeafNodeLMsgSplit(t *testing.T) {
// This set the cluster name to "abc"
oSrv1 := DefaultOptions()
oSrv1.LeafNode.Host = "127.0.0.1"
oSrv1.LeafNode.Port = -1
srv1 := RunServer(oSrv1)
defer srv1.Shutdown()
oSrv2 := DefaultOptions()
oSrv2.LeafNode.Host = "127.0.0.1"
oSrv2.LeafNode.Port = -1
oSrv2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.Cluster.Port))
srv2 := RunServer(oSrv2)
defer srv2.Shutdown()
checkClusterFormed(t, srv1, srv2)
u1, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
u2, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv2.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
oLeaf1 := DefaultOptions()
oLeaf1.Cluster.Name = "xyz"
oLeaf1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u1, u2}}}
leaf1 := RunServer(oLeaf1)
defer leaf1.Shutdown()
oLeaf2 := DefaultOptions()
oLeaf2.Cluster.Name = "xyz"
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u1, u2}}}
oLeaf2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oLeaf1.Cluster.Port))
leaf2 := RunServer(oLeaf2)
defer leaf2.Shutdown()
checkClusterFormed(t, leaf1, leaf2)
checkLeafNodeConnected(t, leaf1)
checkLeafNodeConnected(t, leaf2)
ncSrv2 := natsConnect(t, srv2.ClientURL())
defer ncSrv2.Close()
natsQueueSub(t, ncSrv2, "foo", "queue", func(m *nats.Msg) {
m.Respond([]byte("from srv2"))
})
// Check that "foo" interest is available everywhere.
checkSubInterest(t, srv1, globalAccountName, "foo", time.Second)
checkSubInterest(t, srv2, globalAccountName, "foo", time.Second)
checkSubInterest(t, leaf1, globalAccountName, "foo", time.Second)
checkSubInterest(t, leaf2, globalAccountName, "foo", time.Second)
// Not required, but have a request payload that is more than 100 bytes
reqPayload := make([]byte, 150)
for i := 0; i < len(reqPayload); i++ {
reqPayload[i] = byte((i % 26)) + 'A'
}
// Send repeated requests (from scratch) from leaf-2:
sendReq := func() {
t.Helper()
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
defer ncLeaf2.Close()
if _, err := ncLeaf2.Request("foo", reqPayload, time.Second); err != nil {
t.Fatalf("Did not receive reply: %v", err)
}
}
for i := 0; i < 100; i++ {
sendReq()
}
}
type parseRouteLSUnsubLogger struct {
DummyLogger
gotTrace chan struct{}
gotErr chan error
}
func (l *parseRouteLSUnsubLogger) Errorf(format string, v ...interface{}) {
err := fmt.Errorf(format, v...)
select {
case l.gotErr <- err:
default:
}
}
func (l *parseRouteLSUnsubLogger) Tracef(format string, v ...interface{}) {
trace := fmt.Sprintf(format, v...)
if strings.Contains(trace, "LS- $G foo bar") {
l.gotTrace <- struct{}{}
}
}
func TestLeafNodeRouteParseLSUnsub(t *testing.T) {
// This set the cluster name to "abc"
oSrv1 := DefaultOptions()
oSrv1.LeafNode.Host = "127.0.0.1"
oSrv1.LeafNode.Port = -1
srv1 := RunServer(oSrv1)
defer srv1.Shutdown()
l := &parseRouteLSUnsubLogger{gotTrace: make(chan struct{}, 1), gotErr: make(chan error, 1)}
srv1.SetLogger(l, true, true)
oSrv2 := DefaultOptions()
oSrv2.LeafNode.Host = "127.0.0.1"
oSrv2.LeafNode.Port = -1
oSrv2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.Cluster.Port))
srv2 := RunServer(oSrv2)
defer srv2.Shutdown()
checkClusterFormed(t, srv1, srv2)
u2, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv2.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
oLeaf2 := DefaultOptions()
oLeaf2.Cluster.Name = "xyz"
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u2}}}
leaf2 := RunServer(oLeaf2)
defer leaf2.Shutdown()
checkLeafNodeConnected(t, srv2)
checkLeafNodeConnected(t, leaf2)
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
defer ncLeaf2.Close()
sub := natsQueueSubSync(t, ncLeaf2, "foo", "bar")
// The issue was with the unsubscribe of this queue subscription
natsUnsub(t, sub)
// We should get the trace
select {
case <-l.gotTrace:
// OK!
case <-time.After(100 * time.Millisecond):
t.Fatalf("Did not get LS- trace")
}
// And no error...
select {
case e := <-l.gotErr:
t.Fatalf("There was an error on server 1: %q", e.Error())
case <-time.After(100 * time.Millisecond):
// OK!
}
}
func TestLeafNodeOperatorBadCfg(t *testing.T) {
sysAcc, err := nkeys.CreateAccount()
require_NoError(t, err)
sysAccPk, err := sysAcc.PublicKey()
require_NoError(t, err)
tmpDir := t.TempDir()
configTmpl := `
port: -1
operator: %s
system_account: %s
resolver: {
type: cache
dir: '%s'
}
leafnodes: {
%s
}
`
cases := []struct {
name string
errorText string
cfg string
}{
{
name: "Operator with Leafnode",
errorText: "operator mode does not allow specifying user in leafnode config",
cfg: `
port: -1
authorization {
users = [{user: "u", password: "p"}]}
}`,
},
{
name: "Operator with NKey",
errorText: "operator mode and non account nkeys are incompatible",
cfg: `
port: -1
authorization {
account: notankey
}`,
},
{
name: "Operator remote account NKeys",
errorText: "operator mode requires account nkeys in remotes. " +
"Please add an `account` key to each remote in your `leafnodes` section, to assign it to an account. " +
"Each account value should be a 56 character public key, starting with the letter 'A'",
cfg: `remotes: [{url: u}]`,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
conf := createConfFile(t, []byte(fmt.Sprintf(configTmpl, ojwt, sysAccPk, tmpDir, c.cfg)))
opts := LoadConfig(conf)
s, err := NewServer(opts)
if err == nil {
s.Shutdown()
t.Fatal("Expected an error")
}
// Since the server cannot be stopped, since it did not start,
// let's manually close the account resolver to avoid leaking go routines.
opts.AccountResolver.Close()
if err.Error() != c.errorText {
t.Fatalf("Expected error %s but got %s", c.errorText, err)
}
})
}
}
func TestLeafNodeTLSConfigReload(t *testing.T) {
template := `
listen: 127.0.0.1:-1
leaf {
listen: "127.0.0.1:-1"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
%s
timeout: 2
verify: true
}
}
`
confA := createConfFile(t, []byte(fmt.Sprintf(template, "")))
srvA, optsA := RunServerWithConfig(confA)
defer srvA.Shutdown()
lg := &captureErrorLogger{errCh: make(chan string, 10)}
srvA.SetLogger(lg, false, false)
confB := createConfFile(t, []byte(fmt.Sprintf(`
listen: -1
leaf {
remotes [
{
url: "tls://127.0.0.1:%d"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
}
}
]
}
`, optsA.LeafNode.Port)))
optsB, err := ProcessConfigFile(confB)
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
optsB.LeafNode.ReconnectInterval = 50 * time.Millisecond
optsB.NoLog, optsB.NoSigs = true, true
srvB := RunServer(optsB)
defer srvB.Shutdown()
// Wait for the error
select {
case err := <-lg.errCh:
// Since Go 1.18, we had to regenerate certs to not have to use GODEBUG="x509sha1=1"
// But on macOS, with our test CA certs, no SCTs included, it will fail
// for the reason "x509: “localhost” certificate is not standards compliant"
// instead of "unknown authority".
if !strings.Contains(err, "unknown") && !strings.Contains(err, "compliant") {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatalf("Did not get TLS error")
}
// Add the CA to srvA
reloadUpdateConfig(t, srvA, confA, fmt.Sprintf(template, `ca_file: "../test/configs/certs/ca.pem"`))
// Now make sure that srvB can create a LN connection.
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
if nln := srvB.NumLeafNodes(); nln != 1 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
return nil
})
}
func TestLeafNodeTLSConfigReloadForRemote(t *testing.T) {
confA := createConfFile(t, []byte(`
listen: 127.0.0.1:-1
leaf {
listen: "127.0.0.1:-1"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
timeout: 2
verify: true
}
}
`))
srvA, optsA := RunServerWithConfig(confA)
defer srvA.Shutdown()
lg := &captureErrorLogger{errCh: make(chan string, 10)}
srvA.SetLogger(lg, false, false)
template := `
listen: -1
leaf {
remotes [
{
url: "tls://127.0.0.1:%d"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
%s
}
}
]
}
`
confB := createConfFile(t, []byte(fmt.Sprintf(template, optsA.LeafNode.Port, "")))
srvB, _ := RunServerWithConfig(confB)
defer srvB.Shutdown()
// Wait for the error
select {
case err := <-lg.errCh:
if !strings.Contains(err, "bad certificate") {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatalf("Did not get TLS error")
}
// Add the CA to srvB
reloadUpdateConfig(t, srvB, confB, fmt.Sprintf(template, optsA.LeafNode.Port, `ca_file: "../test/configs/certs/ca.pem"`))
// Now make sure that srvB can create a LN connection.
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
if nln := srvB.NumLeafNodes(); nln != 1 {
return fmt.Errorf("Number of leaf nodes is %d", nln)
}
return nil
})
}
func testDefaultLeafNodeWSOptions() *Options {
o := DefaultOptions()
o.Websocket.Host = "127.0.0.1"
o.Websocket.Port = -1
o.Websocket.NoTLS = true
o.LeafNode.Host = "127.0.0.1"
o.LeafNode.Port = -1
return o
}
func testDefaultRemoteLeafNodeWSOptions(t *testing.T, o *Options, tls bool) *Options {
// Use some path in the URL.. we don't use that, but internally
// the server will prefix the path with /leafnode so that the
// WS webserver knows that it needs to create a LEAF connection.
u, _ := url.Parse(fmt.Sprintf("ws://127.0.0.1:%d/some/path", o.Websocket.Port))
lo := DefaultOptions()
lo.Cluster.Name = "LN"
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
if tls {
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConf, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating TLS config: %v", err)
}
// GenTLSConfig sets the CA in ClientCAs, but since here we act
// as a client, set RootCAs...
tlsConf.RootCAs = tlsConf.ClientCAs
remote.TLSConfig = tlsConf
}
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
return lo
}
func TestLeafNodeWSMixURLs(t *testing.T) {
for _, test := range []struct {
name string
urls []string
}{
{"mix 1", []string{"nats://127.0.0.1:1234", "ws://127.0.0.1:5678", "wss://127.0.0.1:9012"}},
{"mix 2", []string{"ws://127.0.0.1:1234", "nats://127.0.0.1:5678", "wss://127.0.0.1:9012"}},
{"mix 3", []string{"wss://127.0.0.1:1234", "ws://127.0.0.1:5678", "nats://127.0.0.1:9012"}},
{"mix 4", []string{"ws://127.0.0.1:1234", "nats://127.0.0.1:9012"}},
{"mix 5", []string{"nats://127.0.0.1:1234", "ws://127.0.0.1:9012"}},
{"mix 6", []string{"wss://127.0.0.1:1234", "nats://127.0.0.1:9012"}},
{"mix 7", []string{"nats://127.0.0.1:1234", "wss://127.0.0.1:9012"}},
} {
t.Run(test.name, func(t *testing.T) {
o := DefaultOptions()
remote := &RemoteLeafOpts{}
urls := make([]*url.URL, 0, 3)
for _, ustr := range test.urls {
u, err := url.Parse(ustr)
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
urls = append(urls, u)
}
remote.URLs = urls
o.LeafNode.Remotes = []*RemoteLeafOpts{remote}
s, err := NewServer(o)
if err == nil || !strings.Contains(err.Error(), "mix") {
if s != nil {
s.Shutdown()
}
t.Fatalf("Unexpected error: %v", err)
}
})
}
}
type testConnTrackSize struct {
sync.Mutex
net.Conn
sz int
}
func (c *testConnTrackSize) Write(p []byte) (int, error) {
c.Lock()
defer c.Unlock()
n, err := c.Conn.Write(p)
c.sz += n
return n, err
}
func TestLeafNodeWSBasic(t *testing.T) {
for _, test := range []struct {
name string
masking bool
tls bool
acceptCompression bool
remoteCompression bool
}{
{"masking plain no compression", true, false, false, false},
{"masking plain compression", true, false, true, true},
{"masking plain compression disagree", true, false, false, true},
{"masking plain compression disagree 2", true, false, true, false},
{"masking tls no compression", true, true, false, false},
{"masking tls compression", true, true, true, true},
{"masking tls compression disagree", true, true, false, true},
{"masking tls compression disagree 2", true, true, true, false},
{"no masking plain no compression", false, false, false, false},
{"no masking plain compression", false, false, true, true},
{"no masking plain compression disagree", false, false, false, true},
{"no masking plain compression disagree 2", false, false, true, false},
{"no masking tls no compression", false, true, false, false},
{"no masking tls compression", false, true, true, true},
{"no masking tls compression disagree", false, true, false, true},
{"no masking tls compression disagree 2", false, true, true, false},
} {
t.Run(test.name, func(t *testing.T) {
o := testDefaultLeafNodeWSOptions()
o.Websocket.NoTLS = !test.tls
if test.tls {
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConf, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating TLS config: %v", err)
}
o.Websocket.TLSConfig = tlsConf
}
o.Websocket.Compression = test.acceptCompression
s := RunServer(o)
defer s.Shutdown()
lo := testDefaultRemoteLeafNodeWSOptions(t, o, test.tls)
lo.LeafNode.Remotes[0].Websocket.Compression = test.remoteCompression
lo.LeafNode.Remotes[0].Websocket.NoMasking = !test.masking
ln := RunServer(lo)
defer ln.Shutdown()
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln)
var trackSizeConn *testConnTrackSize
if !test.tls {
var cln *client
ln.mu.Lock()
for _, l := range ln.leafs {
cln = l
break
}
ln.mu.Unlock()
cln.mu.Lock()
trackSizeConn = &testConnTrackSize{Conn: cln.nc}
cln.nc = trackSizeConn
cln.mu.Unlock()
}
nc1 := natsConnect(t, s.ClientURL())
defer nc1.Close()
sub1 := natsSubSync(t, nc1, "foo")
natsFlush(t, nc1)
checkSubInterest(t, ln, globalAccountName, "foo", time.Second)
nc2 := natsConnect(t, ln.ClientURL())
defer nc2.Close()
msg1Payload := make([]byte, 2048)
for i := 0; i < len(msg1Payload); i++ {
msg1Payload[i] = 'A'
}
natsPub(t, nc2, "foo", msg1Payload)
msg := natsNexMsg(t, sub1, time.Second)
if !bytes.Equal(msg.Data, msg1Payload) {
t.Fatalf("Invalid message: %q", msg.Data)
}
sub2 := natsSubSync(t, nc2, "bar")
natsFlush(t, nc2)
checkSubInterest(t, s, globalAccountName, "bar", time.Second)
msg2Payload := make([]byte, 2048)
for i := 0; i < len(msg2Payload); i++ {
msg2Payload[i] = 'B'
}
natsPub(t, nc1, "bar", msg2Payload)
msg = natsNexMsg(t, sub2, time.Second)
if !bytes.Equal(msg.Data, msg2Payload) {
t.Fatalf("Invalid message: %q", msg.Data)
}
if !test.tls {
trackSizeConn.Lock()
size := trackSizeConn.sz
trackSizeConn.Unlock()
if test.acceptCompression && test.remoteCompression {
if size >= 1024 {
t.Fatalf("Seems that there was no compression: size=%v", size)
}
} else if size < 2048 {
t.Fatalf("Seems compression was on while it should not: size=%v", size)
}
}
})
}
}
func TestLeafNodeWSRemoteCompressAndMaskingOptions(t *testing.T) {
for _, test := range []struct {
name string
compress bool
compStr string
noMasking bool
noMaskStr string
}{
{"compression masking", true, "true", false, "false"},
{"compression no masking", true, "true", true, "true"},
{"no compression masking", false, "false", false, "false"},
{"no compression no masking", false, "false", true, "true"},
} {
t.Run(test.name, func(t *testing.T) {
conf := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
remotes [
{url: "ws://127.0.0.1:1234", ws_compression: %s, ws_no_masking: %s}
]
}
`, test.compStr, test.noMaskStr)))
o, err := ProcessConfigFile(conf)
if err != nil {
t.Fatalf("Error loading conf: %v", err)
}
if nr := len(o.LeafNode.Remotes); nr != 1 {
t.Fatalf("Expected 1 remote, got %v", nr)
}
r := o.LeafNode.Remotes[0]
if cur := r.Websocket.Compression; cur != test.compress {
t.Fatalf("Expected compress to be %v, got %v", test.compress, cur)
}
if cur := r.Websocket.NoMasking; cur != test.noMasking {
t.Fatalf("Expected ws_masking to be %v, got %v", test.noMasking, cur)
}
})
}
}
func TestLeafNodeWSNoMaskingRejected(t *testing.T) {
wsTestRejectNoMasking = true
defer func() { wsTestRejectNoMasking = false }()
o := testDefaultLeafNodeWSOptions()
s := RunServer(o)
defer s.Shutdown()
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
lo.LeafNode.Remotes[0].Websocket.NoMasking = true
ln := RunServer(lo)
defer ln.Shutdown()
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln)
var cln *client
ln.mu.Lock()
for _, l := range ln.leafs {
cln = l
break
}
ln.mu.Unlock()
cln.mu.Lock()
maskWrite := cln.ws.maskwrite
cln.mu.Unlock()
if !maskWrite {
t.Fatal("Leafnode remote connection should mask writes, it does not")
}
}
func TestLeafNodeWSFailedConnection(t *testing.T) {
o := testDefaultLeafNodeWSOptions()
s := RunServer(o)
defer s.Shutdown()
lo := testDefaultRemoteLeafNodeWSOptions(t, o, true)
lo.LeafNode.ReconnectInterval = 100 * time.Millisecond
ln := RunServer(lo)
defer ln.Shutdown()
el := &captureErrorLogger{errCh: make(chan string, 100)}
ln.SetLogger(el, false, false)
select {
case err := <-el.errCh:
if !strings.Contains(err, "handshake error") {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(time.Second):
t.Fatal("No error reported!")
}
ln.Shutdown()
s.Shutdown()
lst, err := natsListen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Error starting listener: %v", err)
}
defer lst.Close()
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
c, err := lst.Accept()
if err != nil {
return
}
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
if rand.Intn(2) == 1 {
c.Write([]byte("something\r\n"))
}
c.Close()
}
}()
time.Sleep(100 * time.Millisecond)
port := lst.Addr().(*net.TCPAddr).Port
u, _ := url.Parse(fmt.Sprintf("ws://127.0.0.1:%d", port))
lo = DefaultOptions()
lo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
lo.LeafNode.ReconnectInterval = 10 * time.Millisecond
ln, _ = NewServer(lo)
el = &captureErrorLogger{errCh: make(chan string, 100)}
ln.SetLogger(el, false, false)
go func() {
ln.Start()
wg.Done()
}()
timeout := time.NewTimer(time.Second)
for i := 0; i < 10; i++ {
select {
case err := <-el.errCh:
if !strings.Contains(err, "Error soliciting") {
t.Fatalf("Unexpected error: %v", err)
}
case <-timeout.C:
t.Fatal("No error reported!")
}
}
ln.Shutdown()
lst.Close()
wg.Wait()
}
func TestLeafNodeWSAuth(t *testing.T) {
template := `
port: -1
authorization {
users [
{user: "user", pass: "puser", connection_types: ["%s"]}
{user: "leaf", pass: "pleaf", connection_types: ["%s"%s]}
]
}
websocket {
port: -1
no_tls: true
}
leafnodes {
port: -1
}
`
s, o, conf := runReloadServerWithContent(t,
[]byte(fmt.Sprintf(template, jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, "")))
defer s.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 10)}
s.SetLogger(l, false, false)
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
u, _ := url.Parse(fmt.Sprintf("ws://leaf:pleaf@127.0.0.1:%d", o.Websocket.Port))
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
lo.LeafNode.ReconnectInterval = 50 * time.Millisecond
ln := RunServer(lo)
defer ln.Shutdown()
var lasterr string
tm := time.NewTimer(2 * time.Second)
for done := false; !done; {
select {
case lasterr = <-l.errCh:
if strings.Contains(lasterr, "authentication") {
done = true
}
case <-tm.C:
t.Fatalf("Expected auth error, got %v", lasterr)
}
}
ws := fmt.Sprintf(`, "%s"`, jwt.ConnectionTypeLeafnodeWS)
reloadUpdateConfig(t, s, conf, fmt.Sprintf(template,
jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, ws))
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln)
nc1 := natsConnect(t, fmt.Sprintf("nats://user:puser@127.0.0.1:%d", o.Port))
defer nc1.Close()
sub := natsSubSync(t, nc1, "foo")
natsFlush(t, nc1)
checkSubInterest(t, ln, globalAccountName, "foo", time.Second)
nc2 := natsConnect(t, ln.ClientURL())
defer nc2.Close()
natsPub(t, nc2, "foo", []byte("msg1"))
msg := natsNexMsg(t, sub, time.Second)
if md := string(msg.Data); md != "msg1" {
t.Fatalf("Invalid message: %q", md)
}
}
func TestLeafNodeWSGossip(t *testing.T) {
o1 := testDefaultLeafNodeWSOptions()
s1 := RunServer(o1)
defer s1.Shutdown()
// Now connect from a server that knows only about s1
lo := testDefaultRemoteLeafNodeWSOptions(t, o1, false)
lo.LeafNode.ReconnectInterval = 15 * time.Millisecond
ln := RunServer(lo)
defer ln.Shutdown()
checkLeafNodeConnected(t, s1)
checkLeafNodeConnected(t, ln)
// Now add a routed server to s1
o2 := testDefaultLeafNodeWSOptions()
o2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", o1.Cluster.Port))
s2 := RunServer(o2)
defer s2.Shutdown()
// Wait for cluster to form
checkClusterFormed(t, s1, s2)
// Now shutdown s1 and check that ln is able to reconnect to s2.
s1.Shutdown()
checkLeafNodeConnected(t, s2)
checkLeafNodeConnected(t, ln)
// Make sure that the reconnection was as a WS connection, not simply to
// the regular LN port.
var s2lc *client
s2.mu.Lock()
for _, l := range s2.leafs {
s2lc = l
break
}
s2.mu.Unlock()
s2lc.mu.Lock()
isWS := s2lc.isWebsocket()
s2lc.mu.Unlock()
if !isWS {
t.Fatal("Leafnode connection is not websocket!")
}
}
// This test was showing an issue if one set maxBufSize to very small value,
// such as maxBufSize = 10. With such small value, we would get a corruption
// in that LMSG would arrive with missing bytes. We are now always making
// a copy when dealing with messages that are bigger than maxBufSize.
func TestLeafNodeWSNoBufferCorruption(t *testing.T) {
o := testDefaultLeafNodeWSOptions()
s := RunServer(o)
defer s.Shutdown()
lo1 := testDefaultRemoteLeafNodeWSOptions(t, o, false)
lo1.LeafNode.ReconnectInterval = 15 * time.Millisecond
ln1 := RunServer(lo1)
defer ln1.Shutdown()
lo2 := DefaultOptions()
lo2.Cluster.Name = "LN"
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
ln2 := RunServer(lo2)
defer ln2.Shutdown()
checkClusterFormed(t, ln1, ln2)
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln1)
nc := natsConnect(t, s.ClientURL())
defer nc.Close()
sub := natsSubSync(t, nc, "foo")
nc1 := natsConnect(t, ln1.ClientURL())
defer nc1.Close()
nc2 := natsConnect(t, ln2.ClientURL())
defer nc2.Close()
sub2 := natsSubSync(t, nc2, "foo")
checkSubInterest(t, s, globalAccountName, "foo", time.Second)
checkSubInterest(t, ln2, globalAccountName, "foo", time.Second)
checkSubInterest(t, ln1, globalAccountName, "foo", time.Second)
payload := make([]byte, 100*1024)
for i := 0; i < len(payload); i++ {
payload[i] = 'A'
}
natsPub(t, nc1, "foo", payload)
checkMsgRcv := func(sub *nats.Subscription) {
msg := natsNexMsg(t, sub, time.Second)
if !bytes.Equal(msg.Data, payload) {
t.Fatalf("Invalid message content: %q", msg.Data)
}
}
checkMsgRcv(sub2)
checkMsgRcv(sub)
}
func TestLeafNodeWSRemoteNoTLSBlockWithWSSProto(t *testing.T) {
o := testDefaultLeafNodeWSOptions()
o.Websocket.NoTLS = false
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConf, err := GenTLSConfig(tc)
if err != nil {
t.Fatalf("Error generating TLS config: %v", err)
}
o.Websocket.TLSConfig = tlsConf
s := RunServer(o)
defer s.Shutdown()
// The test will make sure that if the protocol is "wss://", a TLS handshake must
// be initiated, regardless of the presence of a TLS config block in config file
// or here directly.
// A bug was causing the absence of TLS config block to initiate a non TLS connection
// even if "wss://" proto was specified, which would lead to "invalid websocket connection"
// errors in the log.
// With the fix, the connection will fail because the remote will fail to verify
// the root CA, but at least, we will make sure that this is not an "invalid websocket connection"
u, _ := url.Parse(fmt.Sprintf("wss://127.0.0.1:%d/some/path", o.Websocket.Port))
lo := DefaultOptions()
lo.Cluster.Name = "LN"
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
lo.LeafNode.ReconnectInterval = 100 * time.Millisecond
ln := RunServer(lo)
defer ln.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 10)}
ln.SetLogger(l, false, false)
select {
case e := <-l.errCh:
if strings.Contains(e, "invalid websocket connection") {
t.Fatalf("The remote did not try to create a TLS connection: %v", e)
}
// OK!
return
case <-time.After(2 * time.Second):
t.Fatal("Connection should fail")
}
}
func TestLeafNodeWSNoAuthUser(t *testing.T) {
conf := createConfFile(t, []byte(`
port: -1
accounts {
A { users [ {user: a, password: a} ]}
B { users [ {user: b, password: b} ]}
}
websocket {
port: -1
no_tls: true
no_auth_user: a
}
leafnodes {
port: -1
}
`))
s, o := RunServerWithConfig(conf)
defer s.Shutdown()
nc1 := natsConnect(t, fmt.Sprintf("nats://a:a@127.0.0.1:%d", o.Port))
defer nc1.Close()
lconf := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
accounts {
A { users [ {user: a, password: a} ]}
B { users [ {user: b, password: b} ]}
}
leafnodes {
remotes [
{
url: "ws://127.0.0.1:%d"
account: A
}
]
}
`, o.Websocket.Port)))
ln, lo := RunServerWithConfig(lconf)
defer ln.Shutdown()
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln)
nc2 := natsConnect(t, fmt.Sprintf("nats://a:a@127.0.0.1:%d", lo.Port))
defer nc2.Close()
sub := natsSubSync(t, nc2, "foo")
natsFlush(t, nc2)
checkSubInterest(t, s, "A", "foo", time.Second)
natsPub(t, nc1, "foo", []byte("msg1"))
msg := natsNexMsg(t, sub, time.Second)
if md := string(msg.Data); md != "msg1" {
t.Fatalf("Invalid message: %q", md)
}
}
func TestLeafNodeStreamImport(t *testing.T) {
o1 := DefaultOptions()
o1.LeafNode.Port = -1
accA := NewAccount("A")
o1.Accounts = []*Account{accA}
o1.Users = []*User{{Username: "a", Password: "a", Account: accA}}
o1.LeafNode.Account = "A"
o1.NoAuthUser = "a"
s1 := RunServer(o1)
defer s1.Shutdown()
o2 := DefaultOptions()
o2.LeafNode.Port = -1
o2.Cluster.Name = "xyz"
accB := NewAccount("B")
if err := accB.AddStreamExport(">", nil); err != nil {
t.Fatalf("Error adding stream export: %v", err)
}
accC := NewAccount("C")
if err := accC.AddStreamImport(accB, ">", ""); err != nil {
t.Fatalf("Error adding stream import: %v", err)
}
o2.Accounts = []*Account{accB, accC}
o2.Users = []*User{{Username: "b", Password: "b", Account: accB}, {Username: "c", Password: "c", Account: accC}}
o2.NoAuthUser = "b"
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
o2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}, LocalAccount: "C"}}
s2 := RunServer(o2)
defer s2.Shutdown()
nc1 := natsConnect(t, s1.ClientURL())
defer nc1.Close()
sub := natsSubSync(t, nc1, "a")
checkSubInterest(t, s2, "C", "a", time.Second)
nc2 := natsConnect(t, s2.ClientURL())
defer nc2.Close()
natsPub(t, nc2, "a", []byte("hello?"))
natsNexMsg(t, sub, time.Second)
}
func TestLeafNodeRouteSubWithOrigin(t *testing.T) {
lo1 := DefaultOptions()
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
lo1.Cluster.Name = "local"
lo1.Cluster.Host = "127.0.0.1"
lo1.Cluster.Port = -1
l1 := RunServer(lo1)
defer l1.Shutdown()
lo2 := DefaultOptions()
lo2.LeafNode.Host = "127.0.0.1"
lo2.LeafNode.Port = -1
lo2.Cluster.Name = "local"
lo2.Cluster.Host = "127.0.0.1"
lo2.Cluster.Port = -1
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
l2 := RunServer(lo2)
defer l2.Shutdown()
checkClusterFormed(t, l1, l2)
u1, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo1.LeafNode.Port))
urls := []*url.URL{u1}
ro1 := DefaultOptions()
ro1.Cluster.Name = "remote"
ro1.Cluster.Host = "127.0.0.1"
ro1.Cluster.Port = -1
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: urls}}
r1 := RunServer(ro1)
defer r1.Shutdown()
checkLeafNodeConnected(t, r1)
nc := natsConnect(t, r1.ClientURL(), nats.NoReconnect())
defer nc.Close()
natsSubSync(t, nc, "foo")
natsQueueSubSync(t, nc, "bar", "baz")
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
checkSubInterest(t, l2, globalAccountName, "bar", time.Second)
// Now shutdown the leafnode and check that any subscription for $G on l2 are gone.
r1.Shutdown()
checkFor(t, time.Second, 15*time.Millisecond, func() error {
acc := l2.GlobalAccount()
if n := acc.TotalSubs(); n != 5 {
return fmt.Errorf("Account %q should have 5 subs, got %v", acc.GetName(), n)
}
return nil
})
}
func TestLeafNodeLoopDetectionWithMultipleClusters(t *testing.T) {
lo1 := DefaultOptions()
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
lo1.Cluster.Name = "local"
lo1.Cluster.Host = "127.0.0.1"
lo1.Cluster.Port = -1
l1 := RunServer(lo1)
defer l1.Shutdown()
lo2 := DefaultOptions()
lo2.LeafNode.Host = "127.0.0.1"
lo2.LeafNode.Port = -1
lo2.Cluster.Name = "local"
lo2.Cluster.Host = "127.0.0.1"
lo2.Cluster.Port = -1
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
l2 := RunServer(lo2)
defer l2.Shutdown()
checkClusterFormed(t, l1, l2)
ro1 := DefaultOptions()
ro1.Cluster.Name = "remote"
ro1.Cluster.Host = "127.0.0.1"
ro1.Cluster.Port = -1
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo1.LeafNode.Port)},
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo2.LeafNode.Port)},
}}}
r1 := RunServer(ro1)
defer r1.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 100)}
r1.SetLogger(l, false, false)
ro2 := DefaultOptions()
ro2.Cluster.Name = "remote"
ro2.Cluster.Host = "127.0.0.1"
ro2.Cluster.Port = -1
ro2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", ro1.Cluster.Port))
ro2.LeafNode.ReconnectInterval = 50 * time.Millisecond
ro2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo1.LeafNode.Port)},
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo2.LeafNode.Port)},
}}}
r2 := RunServer(ro2)
defer r2.Shutdown()
checkClusterFormed(t, r1, r2)
checkLeafNodeConnected(t, r1)
checkLeafNodeConnected(t, r2)
l1.Shutdown()
// Now wait for r1 and r2 to reconnect, they should not have a problem of loop detection.
checkLeafNodeConnected(t, r1)
checkLeafNodeConnected(t, r2)
// Wait and make sure we don't have a loop error
timeout := time.NewTimer(500 * time.Millisecond)
for {
select {
case err := <-l.errCh:
if strings.Contains(err, "Loop detected") {
t.Fatal(err)
}
case <-timeout.C:
// OK, we are done.
return
}
}
}
func TestLeafNodeUnsubOnRouteDisconnect(t *testing.T) {
lo1 := DefaultOptions()
lo1.LeafNode.Host = "127.0.0.1"
lo1.LeafNode.Port = -1
lo1.Cluster.Name = "local"
lo1.Cluster.Host = "127.0.0.1"
lo1.Cluster.Port = -1
l1 := RunServer(lo1)
defer l1.Shutdown()
lo2 := DefaultOptions()
lo2.LeafNode.Host = "127.0.0.1"
lo2.LeafNode.Port = -1
lo2.Cluster.Name = "local"
lo2.Cluster.Host = "127.0.0.1"
lo2.Cluster.Port = -1
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
l2 := RunServer(lo2)
defer l2.Shutdown()
checkClusterFormed(t, l1, l2)
u1, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo1.LeafNode.Port))
u2, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo2.LeafNode.Port))
urls := []*url.URL{u1, u2}
ro1 := DefaultOptions()
// DefaultOptions sets a cluster name, so make sure they are different.
// Also, we don't have r1 and r2 clustered in this test, so set port to 0.
ro1.Cluster.Name = _EMPTY_
ro1.Cluster.Port = 0
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: urls}}
r1 := RunServer(ro1)
defer r1.Shutdown()
ro2 := DefaultOptions()
ro1.Cluster.Name = _EMPTY_
ro2.Cluster.Port = 0
ro2.LeafNode.ReconnectInterval = 50 * time.Millisecond
// Have this one point only to l2
ro2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u2}}}
r2 := RunServer(ro2)
defer r2.Shutdown()
checkLeafNodeConnected(t, r1)
checkLeafNodeConnected(t, r2)
// Create a subscription on r1.
nc := natsConnect(t, r1.ClientURL())
defer nc.Close()
sub := natsSubSync(t, nc, "foo")
natsFlush(t, nc)
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
checkSubInterest(t, r2, globalAccountName, "foo", time.Second)
nc2 := natsConnect(t, r2.ClientURL())
defer nc2.Close()
natsPub(t, nc, "foo", []byte("msg1"))
// Check message received
natsNexMsg(t, sub, time.Second)
// Now shutdown l1, l2 should update subscription interest to r2.
// When r1 reconnects to l2, subscription should be updated too.
l1.Shutdown()
// Wait a bit (so that the check of interest is not OK just because
// the route would not have been yet detected as broken), and check
// interest still present on r2, l2.
time.Sleep(100 * time.Millisecond)
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
checkSubInterest(t, r2, globalAccountName, "foo", time.Second)
// Check again that message received ok
natsPub(t, nc, "foo", []byte("msg2"))
natsNexMsg(t, sub, time.Second)
// Now close client. Interest should disappear on r2. Due to a bug,
// it was not.
nc.Close()
checkFor(t, time.Second, 15*time.Millisecond, func() error {
acc := r2.GlobalAccount()
if n := acc.Interest("foo"); n != 0 {
return fmt.Errorf("Still interest on subject: %v", n)
}
return nil
})
}
func TestLeafNodeNoPingBeforeConnect(t *testing.T) {
o := DefaultOptions()
o.LeafNode.Port = -1
o.LeafNode.AuthTimeout = 0.5
// For this test we need to disable compression, because we do use
// the ping timer instead of the auth timer before the negotiation
// is complete.
o.LeafNode.Compression.Mode = CompressionOff
s := RunServer(o)
defer s.Shutdown()
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
c, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("Error on dial: %v", err)
}
defer c.Close()
// Read the info
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
l, _, err := br.ReadLine()
if err != nil {
t.Fatalf("Error on read: %v", err)
}
if !strings.HasPrefix(string(l), "INFO") {
t.Fatalf("Wrong proto: %q", l)
}
var leaf *client
checkFor(t, time.Second, 15*time.Millisecond, func() error {
s.grMu.Lock()
for _, l := range s.grTmpClients {
leaf = l
break
}
s.grMu.Unlock()
if leaf == nil {
return fmt.Errorf("No leaf connection found")
}
return nil
})
// Make sure that ping timer is not set
leaf.mu.Lock()
ptmrSet := leaf.ping.tmr != nil
leaf.mu.Unlock()
if ptmrSet {
t.Fatal("Ping timer was set before CONNECT was processed")
}
// Send CONNECT
if _, err := c.Write([]byte("CONNECT {}\r\n")); err != nil {
t.Fatalf("Error writing connect: %v", err)
}
// Check that we correctly set the timer now
checkFor(t, time.Second, 15*time.Millisecond, func() error {
leaf.mu.Lock()
ptmrSet := leaf.ping.tmr != nil
leaf.mu.Unlock()
if !ptmrSet {
return fmt.Errorf("Timer still not set")
}
return nil
})
// Reduce the first ping..
leaf.mu.Lock()
leaf.ping.tmr.Reset(15 * time.Millisecond)
leaf.mu.Unlock()
// Now consume that PING (we may get LS+, etc..)
for {
c.SetReadDeadline(time.Now().Add(time.Second))
l, _, err = br.ReadLine()
if err != nil {
t.Fatalf("Error on read: %v", err)
}
if strings.HasPrefix(string(l), "PING") {
checkLeafNodeConnected(t, s)
return
}
}
}
func TestLeafNodeNoMsgLoop(t *testing.T) {
hubConf := `
listen: "127.0.0.1:-1"
accounts {
FOO {
users [
{username: leaf, password: pass}
{username: user, password: pass}
]
}
}
cluster {
name: "hub"
listen: "127.0.0.1:-1"
%s
}
leafnodes {
listen: "127.0.0.1:-1"
authorization {
account: FOO
}
}
`
configS1 := createConfFile(t, []byte(fmt.Sprintf(hubConf, "")))
s1, o1 := RunServerWithConfig(configS1)
defer s1.Shutdown()
configS2S3 := createConfFile(t, []byte(fmt.Sprintf(hubConf, fmt.Sprintf(`routes: ["nats://127.0.0.1:%d"]`, o1.Cluster.Port))))
s2, o2 := RunServerWithConfig(configS2S3)
defer s2.Shutdown()
s3, _ := RunServerWithConfig(configS2S3)
defer s3.Shutdown()
checkClusterFormed(t, s1, s2, s3)
contentLN := `
listen: "127.0.0.1:%d"
accounts {
FOO {
users [
{username: leaf, password: pass}
{username: user, password: pass}
]
}
}
leafnodes {
remotes = [
{
url: "nats://leaf:pass@127.0.0.1:%d"
account: FOO
}
]
}
`
lnconf := createConfFile(t, []byte(fmt.Sprintf(contentLN, -1, o1.LeafNode.Port)))
sl1, slo1 := RunServerWithConfig(lnconf)
defer sl1.Shutdown()
sl2, slo2 := RunServerWithConfig(lnconf)
defer sl2.Shutdown()
checkLeafNodeConnected(t, sl1)
checkLeafNodeConnected(t, sl2)
// Create users on each leafnode
nc1, err := nats.Connect(fmt.Sprintf("nats://user:pass@127.0.0.1:%d", slo1.Port))
if err != nil {
t.Fatalf("Error on connect: %v", err)
}
defer nc1.Close()
rch := make(chan struct{}, 1)
nc2, err := nats.Connect(
fmt.Sprintf("nats://user:pass@127.0.0.1:%d", slo2.Port),
nats.ReconnectWait(50*time.Millisecond),
nats.ReconnectHandler(func(_ *nats.Conn) {
rch <- struct{}{}
}),
)
if err != nil {
t.Fatalf("Error on connect: %v", err)
}
defer nc2.Close()
// Create queue subs on sl2
nc2.QueueSubscribe("foo", "bar", func(_ *nats.Msg) {})
nc2.QueueSubscribe("foo", "bar", func(_ *nats.Msg) {})
nc2.Flush()
// Wait for interest to propagate to sl1
checkSubInterest(t, sl1, "FOO", "foo", 250*time.Millisecond)
// Create sub on sl1
ch := make(chan *nats.Msg, 10)
nc1.Subscribe("foo", func(m *nats.Msg) {
select {
case ch <- m:
default:
}
})
nc1.Flush()
checkSubInterest(t, sl2, "FOO", "foo", 250*time.Millisecond)
// Produce from sl1
nc1.Publish("foo", []byte("msg1"))
// Check message is received by plain sub
select {
case <-ch:
case <-time.After(time.Second):
t.Fatalf("Did not receive message")
}
// Restart leaf node, this time make sure we connect to 2nd server.
sl2.Shutdown()
// Use config file but this time reuse the client port and set the 2nd server for
// the remote leaf node port.
lnconf = createConfFile(t, []byte(fmt.Sprintf(contentLN, slo2.Port, o2.LeafNode.Port)))
sl2, _ = RunServerWithConfig(lnconf)
defer sl2.Shutdown()
checkLeafNodeConnected(t, sl2)
// Wait for client to reconnect
select {
case <-rch:
case <-time.After(time.Second):
t.Fatalf("Did not reconnect")
}
// Produce a new messages
for i := 0; i < 10; i++ {
nc1.Publish("foo", []byte(fmt.Sprintf("msg%d", 2+i)))
// Check sub receives 1 message
select {
case <-ch:
case <-time.After(time.Second):
t.Fatalf("Did not receive message")
}
// Check that there is no more...
select {
case m := <-ch:
t.Fatalf("Loop: received second message %s", m.Data)
case <-time.After(50 * time.Millisecond):
// OK
}
}
}
func TestLeafNodeInterestPropagationDaisychain(t *testing.T) {
aTmpl := `
port: %d
leafnodes {
port: %d
}
}`
confA := createConfFile(t, []byte(fmt.Sprintf(aTmpl, -1, -1)))
sA, _ := RunServerWithConfig(confA)
defer sA.Shutdown()
aPort := sA.opts.Port
aLeafPort := sA.opts.LeafNode.Port
confB := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
port: -1
remotes = [{
url:"nats://127.0.0.1:%d"
}]
}`, aLeafPort)))
sB, _ := RunServerWithConfig(confB)
defer sB.Shutdown()
confC := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
port: -1
remotes = [{url:"nats://127.0.0.1:%d"}]
}`, sB.opts.LeafNode.Port)))
sC, _ := RunServerWithConfig(confC)
defer sC.Shutdown()
checkLeafNodeConnectedCount(t, sC, 1)
checkLeafNodeConnectedCount(t, sB, 2)
checkLeafNodeConnectedCount(t, sA, 1)
ncC := natsConnect(t, sC.ClientURL())
defer ncC.Close()
_, err := ncC.SubscribeSync("foo")
require_NoError(t, err)
require_NoError(t, ncC.Flush())
checkSubInterest(t, sC, "$G", "foo", time.Second)
checkSubInterest(t, sB, "$G", "foo", time.Second)
checkSubInterest(t, sA, "$G", "foo", time.Second)
ncA := natsConnect(t, sA.ClientURL())
defer ncA.Close()
sA.Shutdown()
sA.WaitForShutdown()
confAA := createConfFile(t, []byte(fmt.Sprintf(aTmpl, aPort, aLeafPort)))
sAA, _ := RunServerWithConfig(confAA)
defer sAA.Shutdown()
checkLeafNodeConnectedCount(t, sAA, 1)
checkLeafNodeConnectedCount(t, sB, 2)
checkLeafNodeConnectedCount(t, sC, 1)
checkSubInterest(t, sC, "$G", "foo", time.Second)
checkSubInterest(t, sB, "$G", "foo", time.Second)
checkSubInterest(t, sAA, "$G", "foo", time.Second) // failure issue 2448
}
func TestLeafNodeQueueGroupWithLateLNJoin(t *testing.T) {
/*
Topology: A cluster of leafnodes LN2 and LN3, connect
to a cluster C1, C2.
sub(foo) sub(foo)
\ /
C1 <-> C2
^ ^
| |
LN2 <-> LN3
/ \
sub(foo) sub(foo)
Once the above is set, start LN1 that connects to C1.
sub(foo) sub(foo)
\ /
LN1 -> C1 <-> C2
^ ^
| |
LN2 <-> LN3
/ \
sub(foo) sub(foo)
Remove subs to LN3, C2 and C1.
LN1 -> C1 <-> C2
^ ^
| |
LN2 <-> LN3
/
sub(foo)
Publish from LN1 and verify message is received by sub on LN2.
pub(foo)
\
LN1 -> C1 <-> C2
^ ^
| |
LN2 <-> LN3
/
sub(foo)
*/
co1 := DefaultOptions()
co1.LeafNode.Host = "127.0.0.1"
co1.LeafNode.Port = -1
co1.Cluster.Name = "ngs"
co1.Cluster.Host = "127.0.0.1"
co1.Cluster.Port = -1
c1 := RunServer(co1)
defer c1.Shutdown()
co2 := DefaultOptions()
co2.LeafNode.Host = "127.0.0.1"
co2.LeafNode.Port = -1
co2.Cluster.Name = "ngs"
co2.Cluster.Host = "127.0.0.1"
co2.Cluster.Port = -1
co2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", co1.Cluster.Port))
c2 := RunServer(co2)
defer c2.Shutdown()
checkClusterFormed(t, c1, c2)
lo2 := DefaultOptions()
lo2.Cluster.Name = "local"
lo2.Cluster.Host = "127.0.0.1"
lo2.Cluster.Port = -1
lo2.LeafNode.ReconnectInterval = 50 * time.Millisecond
lo2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co1.LeafNode.Port)}}}}
ln2 := RunServer(lo2)
defer ln2.Shutdown()
lo3 := DefaultOptions()
lo3.Cluster.Name = "local"
lo3.Cluster.Host = "127.0.0.1"
lo3.Cluster.Port = -1
lo3.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo2.Cluster.Port))
lo3.LeafNode.ReconnectInterval = 50 * time.Millisecond
lo3.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co2.LeafNode.Port)}}}}
ln3 := RunServer(lo3)
defer ln3.Shutdown()
checkClusterFormed(t, ln2, ln3)
checkLeafNodeConnected(t, ln2)
checkLeafNodeConnected(t, ln3)
cln2 := natsConnect(t, ln2.ClientURL())
defer cln2.Close()
sln2 := natsQueueSubSync(t, cln2, "foo", "qgroup")
natsFlush(t, cln2)
cln3 := natsConnect(t, ln3.ClientURL())
defer cln3.Close()
sln3 := natsQueueSubSync(t, cln3, "foo", "qgroup")
natsFlush(t, cln3)
cc1 := natsConnect(t, c1.ClientURL())
defer cc1.Close()
sc1 := natsQueueSubSync(t, cc1, "foo", "qgroup")
natsFlush(t, cc1)
cc2 := natsConnect(t, c2.ClientURL())
defer cc2.Close()
sc2 := natsQueueSubSync(t, cc2, "foo", "qgroup")
natsFlush(t, cc2)
checkSubInterest(t, c1, globalAccountName, "foo", time.Second)
checkSubInterest(t, c2, globalAccountName, "foo", time.Second)
checkSubInterest(t, ln2, globalAccountName, "foo", time.Second)
checkSubInterest(t, ln3, globalAccountName, "foo", time.Second)
lo1 := DefaultOptions()
lo1.LeafNode.ReconnectInterval = 50 * time.Millisecond
lo1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co1.LeafNode.Port)}}}}
ln1 := RunServer(lo1)
defer ln1.Shutdown()
checkLeafNodeConnected(t, ln1)
checkSubInterest(t, ln1, globalAccountName, "foo", time.Second)
sln3.Unsubscribe()
natsFlush(t, cln3)
sc2.Unsubscribe()
natsFlush(t, cc2)
sc1.Unsubscribe()
natsFlush(t, cc1)
cln1 := natsConnect(t, ln1.ClientURL())
defer cln1.Close()
natsPub(t, cln1, "foo", []byte("hello"))
natsNexMsg(t, sln2, time.Second)
}
func TestLeafNodeJetStreamDomainMapCrossTalk(t *testing.T) {
accs := `
accounts :{
A:{ jetstream: enable, users:[ {user:a1,password:a1}]},
SYS:{ users:[ {user:s1,password:s1}]},
}
system_account: SYS
`
sd1 := t.TempDir()
confA := createConfFile(t, []byte(fmt.Sprintf(`
listen: 127.0.0.1:-1
%s
jetstream: { domain: da, store_dir: '%s', max_mem: 50Mb, max_file: 50Mb }
leafnodes: {
listen: 127.0.0.1:-1
no_advertise: true
authorization: {
timeout: 0.5
}
}
`, accs, sd1)))
sA, _ := RunServerWithConfig(confA)
defer sA.Shutdown()
sd2 := t.TempDir()
confL := createConfFile(t, []byte(fmt.Sprintf(`
listen: 127.0.0.1:-1
%s
jetstream: { domain: dl, store_dir: '%s', max_mem: 50Mb, max_file: 50Mb }
leafnodes:{
no_advertise: true
remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A},
{url:nats://s1:s1@127.0.0.1:%d, account: SYS}]
}
`, accs, sd2, sA.opts.LeafNode.Port, sA.opts.LeafNode.Port)))
sL, _ := RunServerWithConfig(confL)
defer sL.Shutdown()
ncA := natsConnect(t, sA.ClientURL(), nats.UserInfo("a1", "a1"))
defer ncA.Close()
ncL := natsConnect(t, sL.ClientURL(), nats.UserInfo("a1", "a1"))
defer ncL.Close()
test := func(jsA, jsL nats.JetStreamContext) {
kvA, err := jsA.CreateKeyValue(&nats.KeyValueConfig{Bucket: "bucket"})
require_NoError(t, err)
kvL, err := jsL.CreateKeyValue(&nats.KeyValueConfig{Bucket: "bucket"})
require_NoError(t, err)
_, err = kvA.Put("A", nil)
require_NoError(t, err)
_, err = kvL.Put("L", nil)
require_NoError(t, err)
// check for unwanted cross talk
_, err = kvA.Get("A")
require_NoError(t, err)
_, err = kvA.Get("l")
require_Error(t, err)
require_True(t, err == nats.ErrKeyNotFound)
_, err = kvL.Get("A")
require_Error(t, err)
require_True(t, err == nats.ErrKeyNotFound)
_, err = kvL.Get("L")
require_NoError(t, err)
err = jsA.DeleteKeyValue("bucket")
require_NoError(t, err)
err = jsL.DeleteKeyValue("bucket")
require_NoError(t, err)
}
jsA, err := ncA.JetStream()
require_NoError(t, err)
jsL, err := ncL.JetStream()
require_NoError(t, err)
test(jsA, jsL)
jsAL, err := ncA.JetStream(nats.Domain("dl"))
require_NoError(t, err)
jsLA, err := ncL.JetStream(nats.Domain("da"))
require_NoError(t, err)
test(jsAL, jsLA)
jsAA, err := ncA.JetStream(nats.Domain("da"))
require_NoError(t, err)
jsLL, err := ncL.JetStream(nats.Domain("dl"))
require_NoError(t, err)
test(jsAA, jsLL)
}
type checkLeafMinVersionLogger struct {
DummyLogger
errCh chan string
connCh chan string
}
func (l *checkLeafMinVersionLogger) Errorf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if strings.Contains(msg, "minimum version") {
select {
case l.errCh <- msg:
default:
}
}
}
func (l *checkLeafMinVersionLogger) Noticef(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if strings.Contains(msg, "Leafnode connection created") {
select {
case l.connCh <- msg:
default:
}
}
}
func TestLeafNodeMinVersion(t *testing.T) {
conf := createConfFile(t, []byte(`
port: -1
leafnodes {
port: -1
min_version: 2.8.0
}
`))
s, o := RunServerWithConfig(conf)
defer s.Shutdown()
rconf := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
remotes [
{url: "nats://127.0.0.1:%d" }
]
}
`, o.LeafNode.Port)))
ln, _ := RunServerWithConfig(rconf)
defer ln.Shutdown()
checkLeafNodeConnected(t, s)
checkLeafNodeConnected(t, ln)
ln.Shutdown()
s.Shutdown()
// Now makes sure we validate options, not just config file.
for _, test := range []struct {
name string
version string
err string
}{
{"invalid version", "abc", "semver"},
{"version too low", "2.7.9", "the minimum version should be at least 2.8.0"},
} {
t.Run(test.name, func(t *testing.T) {
o.Port = -1
o.LeafNode.Port = -1
o.LeafNode.MinVersion = test.version
if s, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
if s != nil {
s.Shutdown()
}
t.Fatalf("Expected error to contain %q, got %v", test.err, err)
}
})
}
// Ok, so now to verify that a server rejects a leafnode connection
// we will set the min_version above our current VERSION. So first
// decompose the version:
major, minor, _, err := versionComponents(VERSION)
if err != nil {
t.Fatalf("The current server version %q is not valid: %v", VERSION, err)
}
// Let's make our minimum server an minor version above
mv := fmt.Sprintf("%d.%d.0", major, minor+1)
conf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
port: -1
min_version: "%s"
}
`, mv)))
s, o = RunServerWithConfig(conf)
defer s.Shutdown()
l := &checkLeafMinVersionLogger{errCh: make(chan string, 1), connCh: make(chan string, 1)}
s.SetLogger(l, false, false)
rconf = createConfFile(t, []byte(fmt.Sprintf(`
port: -1
leafnodes {
remotes [
{url: "nats://127.0.0.1:%d" }
]
}
`, o.LeafNode.Port)))
lo := LoadConfig(rconf)
lo.LeafNode.ReconnectInterval = 50 * time.Millisecond
ln = RunServer(lo)
defer ln.Shutdown()
select {
case <-l.connCh:
case <-time.After(time.Second):
t.Fatal("Remote did not try to connect")
}
select {
case <-l.errCh:
case <-time.After(time.Second):
t.Fatal("Did not get the minimum version required error")
}
// Since we have a very small reconnect interval, if the connection was
// closed "right away", then we should have had a reconnect attempt with
// another failure. This should not be the case because the server will
// wait 5s before closing the connection.
select {
case <-l.connCh:
t.Fatal("Should not have tried to reconnect")
case <-time.After(250 * time.Millisecond):
// OK
}
}
func TestLeafNodeStreamAndShadowSubs(t *testing.T) {
hubConf := createConfFile(t, []byte(`
port: -1
leafnodes {
port: -1
authorization: {
user: leaf
password: leaf
account: B
}
}
accounts: {
A: {
users = [{user: usrA, password: usrA}]
exports: [{stream: foo.*.>}]
}
B: {
imports: [{stream: {account: A, subject: foo.*.>}}]
}
}
`))
hub, hubo := RunServerWithConfig(hubConf)
defer hub.Shutdown()
leafConfContet := fmt.Sprintf(`
port: -1
leafnodes {
remotes = [
{
url: "nats-leaf://leaf:leaf@127.0.0.1:%d"
account: B
}
]
}
accounts: {
B: {
exports: [{stream: foo.*.>}]
}
C: {
users: [{user: usrC, password: usrC}]
imports: [{stream: {account: B, subject: foo.bar.>}}]
}
}
`, hubo.LeafNode.Port)
leafConf := createConfFile(t, []byte(leafConfContet))
leafo := LoadConfig(leafConf)
leafo.LeafNode.ReconnectInterval = 50 * time.Millisecond
leaf := RunServer(leafo)
defer leaf.Shutdown()
checkLeafNodeConnected(t, hub)
checkLeafNodeConnected(t, leaf)
subPubAndCheck := func() {
t.Helper()
ncl, err := nats.Connect(leaf.ClientURL(), nats.UserInfo("usrC", "usrC"))
if err != nil {
t.Fatalf("Error connecting: %v", err)
}
defer ncl.Close()
// This will send an LS+ to the "hub" server.
sub, err := ncl.SubscribeSync("foo.*.baz")
if err != nil {
t.Fatalf("Error subscribing: %v", err)
}
ncl.Flush()
ncm, err := nats.Connect(hub.ClientURL(), nats.UserInfo("usrA", "usrA"))
if err != nil {
t.Fatalf("Error connecting: %v", err)
}
defer ncm.Close()
// Try a few times in case subject interest has not propagated yet
for i := 0; i < 5; i++ {
ncm.Publish("foo.bar.baz", []byte("msg"))
if _, err := sub.NextMsg(time.Second); err == nil {
// OK, done!
return
}
}
t.Fatal("Message was not received")
}
subPubAndCheck()
// Now cause a restart of the accepting side so that the leaf connection
// is recreated.
hub.Shutdown()
hub = RunServer(hubo)
defer hub.Shutdown()
checkLeafNodeConnected(t, hub)
checkLeafNodeConnected(t, leaf)
subPubAndCheck()
// Issue a config reload even though we make no modification. There was
// a defect that caused the interest propagation to break.
// Set the ReconnectInterval to the default value so that reload does not complain.
leaf.getOpts().LeafNode.ReconnectInterval = DEFAULT_LEAF_NODE_RECONNECT
reloadUpdateConfig(t, leaf, leafConf, leafConfContet)
// Check again
subPubAndCheck()
}
func TestLeafNodeAuthConfigReload(t *testing.T) {
template := `
listen: 127.0.0.1:-1
accounts { test: {} }
leaf {
listen: "127.0.0.1:7422"
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
}
authorization {
# These are only fields allowed atm.
users = [ { user: test, password: "s3cret1", account: "test" } ]
}
}
`
conf := createConfFile(t, []byte(template))
s, _ := RunServerWithConfig(conf)
defer s.Shutdown()
lg := &captureErrorLogger{errCh: make(chan string, 10)}
s.SetLogger(lg, false, false)
// Reload here should work ok.
reloadUpdateConfig(t, s, conf, template)
}
func TestLeafNodeSignatureCB(t *testing.T) {
content := `
port: -1
server_name: OP
operator = "../test/configs/nkeys/op.jwt"
resolver = MEMORY
listen: "127.0.0.1:-1"
leafnodes {
listen: "127.0.0.1:-1"
}
`
conf := createConfFile(t, []byte(content))
s, opts := RunServerWithConfig(conf)
defer s.Shutdown()
_, akp := createAccount(s)
kp, _ := nkeys.CreateUser()
pub, _ := kp.PublicKey()
nuc := jwt.NewUserClaims(pub)
ujwt, err := nuc.Encode(akp)
if err != nil {
t.Fatalf("Error generating user JWT: %v", err)
}
lopts := &DefaultTestOptions
u, err := url.Parse(fmt.Sprintf("nats://%s:%d", opts.LeafNode.Host, opts.LeafNode.Port))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
return "", nil, fmt.Errorf("on purpose")
}
lopts.LeafNode.Remotes = []*RemoteLeafOpts{remote}
lopts.LeafNode.ReconnectInterval = 100 * time.Millisecond
sl := RunServer(lopts)
defer sl.Shutdown()
slog := &captureErrorLogger{errCh: make(chan string, 10)}
sl.SetLogger(slog, false, false)
// Now check that the leafnode got the error that the callback returned.
select {
case err := <-slog.errCh:
if !strings.Contains(err, "on purpose") {
t.Fatalf("Expected error from cb, got %v", err)
}
case <-time.After(time.Second):
t.Fatal("Did not get expected error")
}
sl.Shutdown()
// Now check what happens if the connection is closed while in the callback.
blockCh := make(chan struct{})
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
<-blockCh
sig, err := kp.Sign(nonce)
return ujwt, sig, err
}
sl = RunServer(lopts)
defer sl.Shutdown()
// Recreate the logger so that we are sure not to have possible previous errors
slog = &captureErrorLogger{errCh: make(chan string, 10)}
sl.SetLogger(slog, false, false)
// Get the leaf connection from the temp clients map and close it.
checkFor(t, time.Second, 15*time.Millisecond, func() error {
var c *client
sl.grMu.Lock()
for _, cli := range sl.grTmpClients {
c = cli
}
sl.grMu.Unlock()
if c == nil {
return fmt.Errorf("Client still not found in temp map")
}
c.closeConnection(ClientClosed)
return nil
})
// Release the callback, and check we get the appropriate error.
close(blockCh)
select {
case err := <-slog.errCh:
if !strings.Contains(err, ErrConnectionClosed.Error()) {
t.Fatalf("Expected error that connection was closed, got %v", err)
}
case <-time.After(time.Second):
t.Fatal("Did not get expected error")
}
sl.Shutdown()
// Change to a good CB and now it should work
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
sig, err := kp.Sign(nonce)
return ujwt, sig, err
}
sl = RunServer(lopts)
defer sl.Shutdown()
checkLeafNodeConnected(t, sl)
}
type testLeafTraceLogger struct {
DummyLogger
ch chan string
}
func (l *testLeafTraceLogger) Tracef(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
// We will sub to 'baz' and to 'bar', so filter on 'ba' prefix.
if strings.Contains(msg, "[LS+ ba") {
select {
case l.ch <- msg:
default:
}
}
}
// Make sure permissioned denied subs do not make it to the leafnode even if existing.
func TestLeafNodePermsSuppressSubs(t *testing.T) {
conf := createConfFile(t, []byte(`
listen: 127.0.0.1:-1
authorization {
PERMS = {
publish = "foo"
subscribe = ["_INBOX.>"]
}
users = [
{user: "user", password: "pass"}
{user: "ln", password: "pass" , permissions: $PERMS }
]
}
no_auth_user: user
leafnodes {
listen: 127.0.0.1:7422
}
`))
lconf := createConfFile(t, []byte(`
listen: 127.0.0.1:-1
leafnodes {
remotes = [ { url: "nats://ln:pass@127.0.0.1" } ]
}
trace = true
`))
s, _ := RunServerWithConfig(conf)
defer s.Shutdown()
// Connect client to the hub.
nc, err := nats.Connect(s.ClientURL())
require_NoError(t, err)
// This should not be seen on leafnode side since we only allow pub to "foo"
_, err = nc.SubscribeSync("baz")
require_NoError(t, err)
ln, _ := RunServerWithConfig(lconf)
defer ln.Shutdown()
// Setup logger to capture trace events.
l := &testLeafTraceLogger{ch: make(chan string, 10)}
ln.SetLogger(l, true, true)
checkLeafNodeConnected(t, ln)
// Need to have ot reconnect to trigger since logger attaches too late.
ln.mu.Lock()
for _, c := range ln.leafs {
c.mu.Lock()
c.nc.Close()
c.mu.Unlock()
}
ln.mu.Unlock()
checkLeafNodeConnectedCount(t, ln, 0)
checkLeafNodeConnectedCount(t, ln, 1)
select {
case msg := <-l.ch:
t.Fatalf("Unexpected LS+ seen on leafnode: %s", msg)
case <-time.After(50 * time.Millisecond):
// OK
}
// Now double check that new subs also do not propagate.
// This behavior was working already.
_, err = nc.SubscribeSync("bar")
require_NoError(t, err)
select {
case msg := <-l.ch:
t.Fatalf("Unexpected LS+ seen on leafnode: %s", msg)
case <-time.After(50 * time.Millisecond):
// OK
}
}
func TestLeafNodeDuplicateMsg(t *testing.T) {
// This involves 2 clusters with leafnodes to each other with a different
// account, and those accounts import/export a subject that caused
// duplicate messages. This test requires static ports since we need to
// have A->B and B->A.
a1Conf := createConfFile(t, []byte(`
cluster : {
name : A
port : -1
}
leafnodes : {
port : 14333
remotes : [{
account : A
urls : [nats://leafa:pwd@127.0.0.1:24333]
}]
}
port : -1
server_name : A_1
accounts:{
A:{
users:[
{user: leafa, password: pwd},
{user: usera, password: usera, permissions: {
publish:{ allow:["iot.b.topic"] }
subscribe:{ allow:["iot.a.topic"] }
}}
]
imports:[
{stream:{account:"B", subject:"iot.a.topic"}}
]
},
B:{
users:[
{user: leafb, password: pwd},
]
exports:[
{stream: "iot.a.topic", accounts: ["A"]}
]
}
}
`))
a1, oa1 := RunServerWithConfig(a1Conf)
defer a1.Shutdown()
a2Conf := createConfFile(t, []byte(fmt.Sprintf(`
cluster : {
name : A
port : -1
routes : [nats://127.0.0.1:%d]
}
leafnodes : {
port : 14334
remotes : [{
account : A
urls : [nats://leafa:pwd@127.0.0.1:24334]
}]
}
port : -1
server_name : A_2
accounts:{
A:{
users:[
{user: leafa, password: pwd},
{user: usera, password: usera, permissions: {
publish:{ allow:["iot.b.topic"] }
subscribe:{ allow:["iot.a.topic"] }
}}
]
imports:[
{stream:{account:"B", subject:"iot.a.topic"}}
]
},
B:{
users:[
{user: leafb, password: pwd},
]
exports:[
{stream: "iot.a.topic", accounts: ["A"]}
]
}
}`, oa1.Cluster.Port)))
a2, _ := RunServerWithConfig(a2Conf)
defer a2.Shutdown()
checkClusterFormed(t, a1, a2)
b1Conf := createConfFile(t, []byte(`
cluster : {
name : B
port : -1
}
leafnodes : {
port : 24333
remotes : [{
account : B
urls : [nats://leafb:pwd@127.0.0.1:14333]
}]
}
port : -1
server_name : B_1
accounts:{
A:{
users:[
{user: leafa, password: pwd},
]
exports:[
{stream: "iot.b.topic", accounts: ["B"]}
]
},
B:{
users:[
{user: leafb, password: pwd},
{user: userb, password: userb, permissions: {
publish:{ allow:["iot.a.topic"] },
subscribe:{ allow:["iot.b.topic"] }
}}
]
imports:[
{stream:{account:"A", subject:"iot.b.topic"}}
]
}
}`))
b1, ob1 := RunServerWithConfig(b1Conf)
defer b1.Shutdown()
b2Conf := createConfFile(t, []byte(fmt.Sprintf(`
cluster : {
name : B
port : -1
routes : [nats://127.0.0.1:%d]
}
leafnodes : {
port : 24334
remotes : [{
account : B
urls : [nats://leafb:pwd@127.0.0.1:14334]
}]
}
port : -1
server_name : B_2
accounts:{
A:{
users:[
{user: leafa, password: pwd},
]
exports:[
{stream: "iot.b.topic", accounts: ["B"]}
]
},
B:{
users:[
{user: leafb, password: pwd},
{user: userb, password: userb, permissions: {
publish:{ allow:["iot.a.topic"] },
subscribe:{ allow:["iot.b.topic"] }
}}
]
imports:[
{stream:{account:"A", subject:"iot.b.topic"}}
]
}
}`, ob1.Cluster.Port)))
b2, _ := RunServerWithConfig(b2Conf)
defer b2.Shutdown()
checkClusterFormed(t, b1, b2)
checkLeafNodeConnectedCount(t, a1, 2)
checkLeafNodeConnectedCount(t, a2, 2)
checkLeafNodeConnectedCount(t, b1, 2)
checkLeafNodeConnectedCount(t, b2, 2)
check := func(t *testing.T, subSrv *Server, pubSrv *Server) {
sc := natsConnect(t, subSrv.ClientURL(), nats.UserInfo("userb", "userb"))
defer sc.Close()
subject := "iot.b.topic"
sub := natsSubSync(t, sc, subject)
// Wait for this to be available in A cluster
checkSubInterest(t, a1, "A", subject, time.Second)
checkSubInterest(t, a2, "A", subject, time.Second)
pb := natsConnect(t, pubSrv.ClientURL(), nats.UserInfo("usera", "usera"))
defer pb.Close()
natsPub(t, pb, subject, []byte("msg"))
natsNexMsg(t, sub, time.Second)
// Should be only 1
if msg, err := sub.NextMsg(100 * time.Millisecond); err == nil {
t.Fatalf("Received duplicate on %q: %s", msg.Subject, msg.Data)
}
}
t.Run("sub_b1_pub_a1", func(t *testing.T) { check(t, b1, a1) })
t.Run("sub_b1_pub_a2", func(t *testing.T) { check(t, b1, a2) })
t.Run("sub_b2_pub_a1", func(t *testing.T) { check(t, b2, a1) })
t.Run("sub_b2_pub_a2", func(t *testing.T) { check(t, b2, a2) })
}
func TestLeafNodeTLSHandshakeFirstVerifyNoInfoSent(t *testing.T) {
confHub := createConfFile(t, []byte(`
port : -1
leafnodes : {
port : -1
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
timeout: 2
handshake_first: true
}
}
`))
s1, o1 := RunServerWithConfig(confHub)
defer s1.Shutdown()
c, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", o1.LeafNode.Port), 2*time.Second)
require_NoError(t, err)
defer c.Close()
buf := make([]byte, 1024)
// We will wait for up to 500ms to see if the server is sending (incorrectly)
// the INFO.
c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
n, err := c.Read(buf)
c.SetReadDeadline(time.Time{})
// If we did not get an error, this is an issue...
if err == nil {
t.Fatalf("Should not have received anything, got n=%v buf=%s", n, buf[:n])
}
// We expect a timeout error
if ne, ok := err.(net.Error); !ok || !ne.Timeout() {
t.Fatalf("Expected a timeout error, got %v", err)
}
}
func TestLeafNodeTLSHandshakeFirst(t *testing.T) {
tmpl1 := `
port : -1
leafnodes : {
port : -1
tls {
cert_file: "../test/configs/certs/server-cert.pem"
key_file: "../test/configs/certs/server-key.pem"
ca_file: "../test/configs/certs/ca.pem"
timeout: 2
handshake_first: %s
}
}
`
confHub := createConfFile(t, []byte(fmt.Sprintf(tmpl1, "true")))
s1, o1 := RunServerWithConfig(confHub)
defer s1.Shutdown()
tmpl2 := `
port: -1
leafnodes : {
port : -1
remotes : [
{
urls : [tls://127.0.0.1:%d]
tls {
cert_file: "../test/configs/certs/client-cert.pem"
key_file: "../test/configs/certs/client-key.pem"
ca_file: "../test/configs/certs/ca.pem"
timeout: 2
first: %s
}
}
]
}
`
confSpoke := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, "true")))
s2, _ := RunServerWithConfig(confSpoke)
defer s2.Shutdown()
checkLeafNodeConnected(t, s2)
s2.Shutdown()
// Now check that there will be a failure if the remote does not ask for
// handshake first since the hub is configured that way.
// Set a logger on s1 to capture errors
l := &captureErrorLogger{errCh: make(chan string, 10)}
s1.SetLogger(l, false, false)
confSpoke = createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, "false")))
s2, _ = RunServerWithConfig(confSpoke)
defer s2.Shutdown()
select {
case err := <-l.errCh:
if !strings.Contains(err, "handshake error") {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Did not get TLS handshake failure")
}
// Check configuration reload for this remote
reloadUpdateConfig(t, s2, confSpoke, fmt.Sprintf(tmpl2, o1.LeafNode.Port, "true"))
checkLeafNodeConnected(t, s2)
s2.Shutdown()
// Drain the logger error channel
for done := false; !done; {
select {
case <-l.errCh:
default:
done = true
}
}
// Now change the config on the hub
reloadUpdateConfig(t, s1, confHub, fmt.Sprintf(tmpl1, "false"))
// Restart s2
s2, _ = RunServerWithConfig(confSpoke)
defer s2.Shutdown()
select {
case err := <-l.errCh:
if !strings.Contains(err, "handshake error") {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Did not get TLS handshake failure")
}
// Reload again with "true"
reloadUpdateConfig(t, s1, confHub, fmt.Sprintf(tmpl1, "true"))
checkLeafNodeConnected(t, s2)
}
func TestLeafNodeCompressionOptions(t *testing.T) {
org := testDefaultLeafNodeCompression
testDefaultLeafNodeCompression = _EMPTY_
defer func() { testDefaultLeafNodeCompression = org }()
tmpl := `
port: -1
leafnodes {
port: -1
compression: %s
}
`
for _, test := range []struct {
name string
mode string
rttVals []int
expected string
rtts []time.Duration
}{
{"boolean enabled", "true", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string enabled", "enabled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string EnaBled", "EnaBled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string on", "on", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string ON", "ON", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string fast", "fast", nil, CompressionS2Fast, nil},
{"string Fast", "Fast", nil, CompressionS2Fast, nil},
{"string s2_fast", "s2_fast", nil, CompressionS2Fast, nil},
{"string s2_Fast", "s2_Fast", nil, CompressionS2Fast, nil},
{"boolean disabled", "false", nil, CompressionOff, nil},
{"string disabled", "disabled", nil, CompressionOff, nil},
{"string DisableD", "DisableD", nil, CompressionOff, nil},
{"string off", "off", nil, CompressionOff, nil},
{"string OFF", "OFF", nil, CompressionOff, nil},
{"better", "better", nil, CompressionS2Better, nil},
{"Better", "Better", nil, CompressionS2Better, nil},
{"s2_better", "s2_better", nil, CompressionS2Better, nil},
{"S2_BETTER", "S2_BETTER", nil, CompressionS2Better, nil},
{"best", "best", nil, CompressionS2Best, nil},
{"BEST", "BEST", nil, CompressionS2Best, nil},
{"s2_best", "s2_best", nil, CompressionS2Best, nil},
{"S2_BEST", "S2_BEST", nil, CompressionS2Best, nil},
{"auto no rtts", "auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"s2_auto no rtts", "s2_auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"auto", "{mode: auto, rtt_thresholds: [%s]}", []int{1}, CompressionS2Auto, []time.Duration{time.Millisecond}},
{"Auto", "{Mode: Auto, thresholds: [%s]}", []int{1, 2}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond}},
{"s2_auto", "{mode: s2_auto, thresholds: [%s]}", []int{1, 2, 3}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond}},
{"s2_AUTO", "{mode: s2_AUTO, thresholds: [%s]}", []int{1, 2, 3, 4}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond, 4 * time.Millisecond}},
{"s2_auto:-10,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{-10, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
{"s2_auto:5,10,15", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 15}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 15 * time.Millisecond}},
{"s2_auto:0,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{0, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
{"s2_auto:5,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 0, 20}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
{"s2_auto:0,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 10, 0, 20}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
{"s2_auto:0,0,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 0, 0, 20}, CompressionS2Auto, []time.Duration{0, 0, 0, 20 * time.Millisecond}},
{"s2_auto:0,10,0,0", "{mode: s2_auto, rtt_thresholds: [%s]}", []int{0, 10, 0, 0}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond}},
} {
t.Run(test.name, func(t *testing.T) {
var val string
if len(test.rttVals) > 0 {
var rtts string
for i, v := range test.rttVals {
if i > 0 {
rtts += ", "
}
rtts += fmt.Sprintf("%dms", v)
}
val = fmt.Sprintf(test.mode, rtts)
} else {
val = test.mode
}
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, val)))
s, o := RunServerWithConfig(conf)
defer s.Shutdown()
if cm := o.LeafNode.Compression.Mode; cm != test.expected {
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
}
if !reflect.DeepEqual(test.rtts, o.LeafNode.Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Compression.RTTThresholds)
}
s.Shutdown()
o.LeafNode.Port = -1
o.LeafNode.Compression.Mode = test.mode
if len(test.rttVals) > 0 {
o.LeafNode.Compression.Mode = CompressionS2Auto
o.LeafNode.Compression.RTTThresholds = o.LeafNode.Compression.RTTThresholds[:0]
for _, v := range test.rttVals {
o.LeafNode.Compression.RTTThresholds = append(o.LeafNode.Compression.RTTThresholds, time.Duration(v)*time.Millisecond)
}
}
s = RunServer(o)
defer s.Shutdown()
if cm := o.LeafNode.Compression.Mode; cm != test.expected {
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
}
if !reflect.DeepEqual(test.rtts, o.LeafNode.Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Compression.RTTThresholds)
}
})
}
// Same, but with remotes
tmpl = `
port: -1
leafnodes {
port: -1
remotes [
{
url: "nats://127.0.0.1:1234"
compression: %s
}
]
}
`
for _, test := range []struct {
name string
mode string
rttVals []int
expected string
rtts []time.Duration
}{
{"boolean enabled", "true", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string enabled", "enabled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string EnaBled", "EnaBled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string on", "on", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string ON", "ON", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"string fast", "fast", nil, CompressionS2Fast, nil},
{"string Fast", "Fast", nil, CompressionS2Fast, nil},
{"string s2_fast", "s2_fast", nil, CompressionS2Fast, nil},
{"string s2_Fast", "s2_Fast", nil, CompressionS2Fast, nil},
{"boolean disabled", "false", nil, CompressionOff, nil},
{"string disabled", "disabled", nil, CompressionOff, nil},
{"string DisableD", "DisableD", nil, CompressionOff, nil},
{"string off", "off", nil, CompressionOff, nil},
{"string OFF", "OFF", nil, CompressionOff, nil},
{"better", "better", nil, CompressionS2Better, nil},
{"Better", "Better", nil, CompressionS2Better, nil},
{"s2_better", "s2_better", nil, CompressionS2Better, nil},
{"S2_BETTER", "S2_BETTER", nil, CompressionS2Better, nil},
{"best", "best", nil, CompressionS2Best, nil},
{"BEST", "BEST", nil, CompressionS2Best, nil},
{"s2_best", "s2_best", nil, CompressionS2Best, nil},
{"S2_BEST", "S2_BEST", nil, CompressionS2Best, nil},
{"auto no rtts", "auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"s2_auto no rtts", "s2_auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
{"auto", "{mode: auto, rtt_thresholds: [%s]}", []int{1}, CompressionS2Auto, []time.Duration{time.Millisecond}},
{"Auto", "{Mode: Auto, thresholds: [%s]}", []int{1, 2}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond}},
{"s2_auto", "{mode: s2_auto, thresholds: [%s]}", []int{1, 2, 3}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond}},
{"s2_AUTO", "{mode: s2_AUTO, thresholds: [%s]}", []int{1, 2, 3, 4}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond, 4 * time.Millisecond}},
{"s2_auto:-10,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{-10, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
{"s2_auto:5,10,15", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 15}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 15 * time.Millisecond}},
{"s2_auto:0,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{0, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
{"s2_auto:5,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 0, 20}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
{"s2_auto:0,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 10, 0, 20}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
{"s2_auto:0,0,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 0, 0, 20}, CompressionS2Auto, []time.Duration{0, 0, 0, 20 * time.Millisecond}},
{"s2_auto:0,10,0,0", "{mode: s2_auto, rtt_thresholds: [%s]}", []int{0, 10, 0, 0}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond}},
} {
t.Run("remote leaf "+test.name, func(t *testing.T) {
var val string
if len(test.rttVals) > 0 {
var rtts string
for i, v := range test.rttVals {
if i > 0 {
rtts += ", "
}
rtts += fmt.Sprintf("%dms", v)
}
val = fmt.Sprintf(test.mode, rtts)
} else {
val = test.mode
}
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, val)))
s, o := RunServerWithConfig(conf)
defer s.Shutdown()
r := o.LeafNode.Remotes[0]
if cm := r.Compression.Mode; cm != test.expected {
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
}
if !reflect.DeepEqual(test.rtts, r.Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, r.Compression.RTTThresholds)
}
s.Shutdown()
o.LeafNode.Port = -1
o.LeafNode.Remotes[0].Compression.Mode = test.mode
if len(test.rttVals) > 0 {
o.LeafNode.Remotes[0].Compression.Mode = CompressionS2Auto
o.LeafNode.Remotes[0].Compression.RTTThresholds = o.LeafNode.Remotes[0].Compression.RTTThresholds[:0]
for _, v := range test.rttVals {
o.LeafNode.Remotes[0].Compression.RTTThresholds = append(o.LeafNode.Remotes[0].Compression.RTTThresholds, time.Duration(v)*time.Millisecond)
}
}
s = RunServer(o)
defer s.Shutdown()
if cm := o.LeafNode.Remotes[0].Compression.Mode; cm != test.expected {
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
}
if !reflect.DeepEqual(test.rtts, o.LeafNode.Remotes[0].Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Remotes[0].Compression.RTTThresholds)
}
})
}
// Test that with no compression specified, we default to "s2_auto"
conf := createConfFile(t, []byte(`
port: -1
leafnodes {
port: -1
}
`))
s, o := RunServerWithConfig(conf)
defer s.Shutdown()
if o.LeafNode.Compression.Mode != CompressionS2Auto {
t.Fatalf("Expected compression value to be %q, got %q", CompressionAccept, o.LeafNode.Compression.Mode)
}
if !reflect.DeepEqual(defaultCompressionS2AutoRTTThresholds, o.LeafNode.Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", defaultCompressionS2AutoRTTThresholds, o.LeafNode.Compression.RTTThresholds)
}
// Same for remotes
conf = createConfFile(t, []byte(`
port: -1
leafnodes {
port: -1
remotes [ { url: "nats://127.0.0.1:1234" } ]
}
`))
s, o = RunServerWithConfig(conf)
defer s.Shutdown()
if cm := o.LeafNode.Remotes[0].Compression.Mode; cm != CompressionS2Auto {
t.Fatalf("Expected compression value to be %q, got %q", CompressionAccept, cm)
}
if !reflect.DeepEqual(defaultCompressionS2AutoRTTThresholds, o.LeafNode.Remotes[0].Compression.RTTThresholds) {
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", defaultCompressionS2AutoRTTThresholds, o.LeafNode.Remotes[0].Compression.RTTThresholds)
}
for _, test := range []struct {
name string
mode string
rtts []time.Duration
err string
}{
{"unsupported mode", "gzip", nil, "Unsupported"},
{"not ascending order", "s2_auto", []time.Duration{
5 * time.Millisecond,
10 * time.Millisecond,
2 * time.Millisecond,
}, "ascending"},
{"too many thresholds", "s2_auto", []time.Duration{
5 * time.Millisecond,
10 * time.Millisecond,
20 * time.Millisecond,
40 * time.Millisecond,
60 * time.Millisecond,
}, "more than 4"},
{"all 0", "s2_auto", []time.Duration{0, 0, 0, 0}, "at least one"},
{"single 0", "s2_auto", []time.Duration{0}, "at least one"},
} {
t.Run(test.name, func(t *testing.T) {
o := DefaultOptions()
o.LeafNode.Port = -1
o.LeafNode.Compression = CompressionOpts{test.mode, test.rtts}
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
t.Fatalf("Unexpected error: %v", err)
}
// Same with remotes
o.LeafNode.Compression = CompressionOpts{}
o.LeafNode.Remotes = []*RemoteLeafOpts{{Compression: CompressionOpts{test.mode, test.rtts}}}
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
t.Fatalf("Unexpected error: %v", err)
}
})
}
}
func TestLeafNodeCompression(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
server_name: "Hub"
accounts {
A { users: [{user: a, password: pwd}] }
B { users: [{user: b, password: pwd}] }
C { users: [{user: c, password: pwd}] }
}
leafnodes {
port: -1
compression: s2_fast
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
port := o1.LeafNode.Port
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "Spoke"
accounts {
A { users: [{user: a, password: pwd}] }
B { users: [{user: b, password: pwd}] }
C { users: [{user: c, password: pwd}] }
}
leafnodes {
remotes [
{ url: "nats://a:pwd@127.0.0.1:%d", account: "A", compression: s2_better }
{ url: "nats://b:pwd@127.0.0.1:%d", account: "B", compression: s2_best }
{ url: "nats://c:pwd@127.0.0.1:%d", account: "C", compression: off }
]
}
`, port, port, port)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
checkLeafNodeConnectedCount(t, s1, 3)
checkLeafNodeConnectedCount(t, s2, 3)
s1.mu.RLock()
for _, l := range s1.leafs {
l.mu.Lock()
l.nc = &testConnSentBytes{Conn: l.nc}
l.mu.Unlock()
}
s1.mu.RUnlock()
var payloads [][]byte
totalPayloadSize := 0
count := 26
for i := 0; i < count; i++ {
n := rand.Intn(2048) + 1
p := make([]byte, n)
for j := 0; j < n; j++ {
p[j] = byte(i) + 'A'
}
totalPayloadSize += len(p)
payloads = append(payloads, p)
}
check := func(acc, user, subj string) {
t.Helper()
nc2 := natsConnect(t, s2.ClientURL(), nats.UserInfo(user, "pwd"))
defer nc2.Close()
sub := natsSubSync(t, nc2, subj)
natsFlush(t, nc2)
checkSubInterest(t, s1, acc, subj, time.Second)
nc1 := natsConnect(t, s1.ClientURL(), nats.UserInfo(user, "pwd"))
defer nc1.Close()
for i := 0; i < count; i++ {
natsPub(t, nc1, subj, payloads[i])
}
for i := 0; i < count; i++ {
m := natsNexMsg(t, sub, time.Second)
if !bytes.Equal(m.Data, payloads[i]) {
t.Fatalf("Expected payload %q - got %q", payloads[i], m.Data)
}
}
// Also check that the leafnode stats shows that compression likely occurred
var out int
s1.mu.RLock()
for _, l := range s1.leafs {
l.mu.Lock()
if l.acc.Name == acc && l.nc != nil {
nc := l.nc.(*testConnSentBytes)
nc.Lock()
out = nc.sent
nc.sent = 0
nc.Unlock()
}
l.mu.Unlock()
}
s1.mu.RUnlock()
// Except for account "C", where compression should be off,
// "out" should at least be smaller than totalPayloadSize, use 20%.
if acc == "C" {
if int(out) < totalPayloadSize {
t.Fatalf("Expected s1's sent bytes to be at least payload size (%v), got %v", totalPayloadSize, out)
}
} else {
limit := totalPayloadSize * 80 / 100
if int(out) > limit {
t.Fatalf("Expected s1's sent bytes to be less than %v, got %v (total payload was %v)", limit, out, totalPayloadSize)
}
}
}
check("A", "a", "foo")
check("B", "b", "bar")
check("C", "c", "baz")
// Check compression settings. S1 should always be s2_fast, except for account "C"
// since "C" wanted compression "off"
l, err := s1.Leafz(nil)
require_NoError(t, err)
for _, r := range l.Leafs {
switch r.Account {
case "C":
if r.Compression != CompressionOff {
t.Fatalf("Expected compression of remote for C account to be %q, got %q", CompressionOff, r.Compression)
}
default:
if r.Compression != CompressionS2Fast {
t.Fatalf("Expected compression of remote for %s account to be %q, got %q", r.Account, CompressionS2Fast, r.Compression)
}
}
}
l, err = s2.Leafz(nil)
require_NoError(t, err)
for _, r := range l.Leafs {
switch r.Account {
case "A":
if r.Compression != CompressionS2Better {
t.Fatalf("Expected compression for A account to be %q, got %q", CompressionS2Better, r.Compression)
}
case "B":
if r.Compression != CompressionS2Best {
t.Fatalf("Expected compression for B account to be %q, got %q", CompressionS2Best, r.Compression)
}
case "C":
if r.Compression != CompressionOff {
t.Fatalf("Expected compression for C account to be %q, got %q", CompressionOff, r.Compression)
}
}
}
}
func TestLeafNodeCompressionMatrixModes(t *testing.T) {
for _, test := range []struct {
name string
s1 string
s2 string
s1Expected string
s2Expected string
}{
{"off off", "off", "off", CompressionOff, CompressionOff},
{"off accept", "off", "accept", CompressionOff, CompressionOff},
{"off on", "off", "on", CompressionOff, CompressionOff},
{"off better", "off", "better", CompressionOff, CompressionOff},
{"off best", "off", "best", CompressionOff, CompressionOff},
{"accept off", "accept", "off", CompressionOff, CompressionOff},
{"accept accept", "accept", "accept", CompressionOff, CompressionOff},
// Note: "on", means s2_auto, which will mean uncompressed since RTT is low.
{"accept on", "accept", "on", CompressionS2Fast, CompressionS2Uncompressed},
{"accept better", "accept", "better", CompressionS2Better, CompressionS2Better},
{"accept best", "accept", "best", CompressionS2Best, CompressionS2Best},
{"on off", "on", "off", CompressionOff, CompressionOff},
{"on accept", "on", "accept", CompressionS2Uncompressed, CompressionS2Fast},
{"on on", "on", "on", CompressionS2Uncompressed, CompressionS2Uncompressed},
{"on better", "on", "better", CompressionS2Uncompressed, CompressionS2Better},
{"on best", "on", "best", CompressionS2Uncompressed, CompressionS2Best},
{"better off", "better", "off", CompressionOff, CompressionOff},
{"better accept", "better", "accept", CompressionS2Better, CompressionS2Better},
{"better on", "better", "on", CompressionS2Better, CompressionS2Uncompressed},
{"better better", "better", "better", CompressionS2Better, CompressionS2Better},
{"better best", "better", "best", CompressionS2Better, CompressionS2Best},
{"best off", "best", "off", CompressionOff, CompressionOff},
{"best accept", "best", "accept", CompressionS2Best, CompressionS2Best},
{"best on", "best", "on", CompressionS2Best, CompressionS2Uncompressed},
{"best better", "best", "better", CompressionS2Best, CompressionS2Better},
{"best best", "best", "best", CompressionS2Best, CompressionS2Best},
} {
t.Run(test.name, func(t *testing.T) {
conf1 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "A"
leafnodes {
port: -1
compression: %s
}
`, test.s1)))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "B"
leafnodes {
remotes: [
{url: "nats://127.0.0.1:%d", compression: %s}
]
}
`, o1.LeafNode.Port, test.s2)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
checkLeafNodeConnected(t, s2)
nc1 := natsConnect(t, s1.ClientURL())
defer nc1.Close()
nc2 := natsConnect(t, s2.ClientURL())
defer nc2.Close()
payload := make([]byte, 128)
check := func(ncp, ncs *nats.Conn, subj string, s *Server) {
t.Helper()
sub := natsSubSync(t, ncs, subj)
checkSubInterest(t, s, globalAccountName, subj, time.Second)
natsPub(t, ncp, subj, payload)
natsNexMsg(t, sub, time.Second)
for _, srv := range []*Server{s1, s2} {
lz, err := srv.Leafz(nil)
require_NoError(t, err)
var expected string
if srv == s1 {
expected = test.s1Expected
} else {
expected = test.s2Expected
}
if cm := lz.Leafs[0].Compression; cm != expected {
t.Fatalf("Server %s - expected compression %q, got %q", srv, expected, cm)
}
}
}
check(nc1, nc2, "foo", s1)
check(nc2, nc1, "bar", s2)
})
}
}
func TestLeafNodeCompressionWithOlderServer(t *testing.T) {
tmpl1 := `
port: -1
server_name: "A"
leafnodes {
port: -1
compression: "%s"
}
`
conf1 := createConfFile(t, []byte(fmt.Sprintf(tmpl1, CompressionS2Fast)))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
tmpl2 := `
port: -1
server_name: "B"
leafnodes {
remotes [
{url: "nats://127.0.0.1:%d", compression: "%s"}
]
}
`
conf2 := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, CompressionNotSupported)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
checkLeafNodeConnected(t, s2)
getLeafCompMode := func(s *Server) string {
var cm string
s.mu.RLock()
defer s.mu.RUnlock()
for _, l := range s1.leafs {
l.mu.Lock()
cm = l.leaf.compression
l.mu.Unlock()
return cm
}
return _EMPTY_
}
for _, s := range []*Server{s1, s2} {
if cm := getLeafCompMode(s); cm != CompressionNotSupported {
t.Fatalf("Expected compression not supported, got %q", cm)
}
}
s2.Shutdown()
s1.Shutdown()
conf1 = createConfFile(t, []byte(fmt.Sprintf(tmpl1, CompressionNotSupported)))
s1, o1 = RunServerWithConfig(conf1)
defer s1.Shutdown()
conf2 = createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, CompressionS2Fast)))
s2, _ = RunServerWithConfig(conf2)
defer s2.Shutdown()
checkLeafNodeConnected(t, s2)
for _, s := range []*Server{s1, s2} {
if cm := getLeafCompMode(s); cm != CompressionNotSupported {
t.Fatalf("Expected compression not supported, got %q", cm)
}
}
}
func TestLeafNodeCompressionAuto(t *testing.T) {
for _, test := range []struct {
name string
s1Ping string
s1Compression string
s2Ping string
s2Compression string
checkS1 bool
}{
{"remote side", "10s", CompressionS2Fast, "100ms", "{mode: s2_auto, rtt_thresholds: [10ms, 20ms, 30ms]}", false},
{"accept side", "100ms", "{mode: s2_auto, rtt_thresholds: [10ms, 20ms, 30ms]}", "10s", CompressionS2Fast, true},
} {
t.Run(test.name, func(t *testing.T) {
conf1 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "A"
ping_interval: "%s"
leafnodes {
port: -1
compression: %s
}
`, test.s1Ping, test.s1Compression)))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
// Start with 0ms RTT
np := createNetProxy(0, 1024*1024*1024, 1024*1024*1024, fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port), true)
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "B"
ping_interval: "%s"
leafnodes {
remotes [
{url: %s, compression %s}
]
}
`, test.s2Ping, np.routeURL(), test.s2Compression)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
defer np.stop()
checkLeafNodeConnected(t, s2)
checkComp := func(expected string) {
t.Helper()
var s *Server
if test.checkS1 {
s = s1
} else {
s = s2
}
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, l := range s.leafs {
l.mu.Lock()
cm := l.leaf.compression
l.mu.Unlock()
if cm != expected {
return fmt.Errorf("Leaf %v compression mode expected to be %q, got %q", l, expected, cm)
}
}
return nil
})
}
checkComp(CompressionS2Uncompressed)
// Change the proxy RTT and we should get compression "fast"
np.updateRTT(15 * time.Millisecond)
checkComp(CompressionS2Fast)
// Now 25ms, and get "better"
np.updateRTT(25 * time.Millisecond)
checkComp(CompressionS2Better)
// Above 35 and we should get "best"
np.updateRTT(35 * time.Millisecond)
checkComp(CompressionS2Best)
// Down to 1ms and again should get "uncompressed"
np.updateRTT(1 * time.Millisecond)
checkComp(CompressionS2Uncompressed)
})
}
// Make sure that if compression is off on one side, the update of RTT does
// not trigger a compression change.
conf1 := createConfFile(t, []byte(`
port: -1
server_name: "A"
leafnodes {
port: -1
compression: off
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
// Start with 0ms RTT
np := createNetProxy(0, 1024*1024*1024, 1024*1024*1024, fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port), true)
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "B"
ping_interval: "50ms"
leafnodes {
remotes [
{url: %s, compression s2_auto}
]
}
`, np.routeURL())))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
defer np.stop()
checkLeafNodeConnected(t, s2)
// Even with a bug of updating compression level while it should have been
// off, the check done below would almost always pass because after
// reconnecting, there could be a chance to get at first compression set
// to "off". So we will double check that the leaf node CID did not change
// at the end of the test.
getCID := func() uint64 {
s2.mu.RLock()
defer s2.mu.RUnlock()
for _, l := range s2.leafs {
l.mu.Lock()
cid := l.cid
l.mu.Unlock()
return cid
}
return 0
}
oldCID := getCID()
checkCompOff := func() {
t.Helper()
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
s2.mu.RLock()
defer s2.mu.RUnlock()
if len(s2.leafs) != 1 {
return fmt.Errorf("Leaf not currently connected")
}
for _, l := range s2.leafs {
l.mu.Lock()
cm := l.leaf.compression
l.mu.Unlock()
if cm != CompressionOff {
return fmt.Errorf("Leaf %v compression mode expected to be %q, got %q", l, CompressionOff, cm)
}
}
return nil
})
}
checkCompOff()
// Now change RTT and again, make sure that it is still off
np.updateRTT(20 * time.Millisecond)
time.Sleep(100 * time.Millisecond)
checkCompOff()
if cid := getCID(); cid != oldCID {
t.Fatalf("Leafnode has reconnected, cid was %v, now %v", oldCID, cid)
}
}
func TestLeafNodeCompressionWithWSCompression(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
server_name: "A"
websocket {
port: -1
no_tls: true
compression: true
}
leafnodes {
port: -1
compression: s2_fast
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "B"
leafnodes {
remotes [
{
url: "ws://127.0.0.1:%d"
ws_compression: true
compression: s2_fast
}
]
}
`, o1.Websocket.Port)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
checkLeafNodeConnected(t, s2)
nc1 := natsConnect(t, s1.ClientURL())
defer nc1.Close()
sub := natsSubSync(t, nc1, "foo")
checkSubInterest(t, s2, globalAccountName, "foo", time.Second)
nc2 := natsConnect(t, s2.ClientURL())
defer nc2.Close()
payload := make([]byte, 1024)
for i := 0; i < len(payload); i++ {
payload[i] = 'A'
}
natsPub(t, nc2, "foo", payload)
msg := natsNexMsg(t, sub, time.Second)
require_True(t, len(msg.Data) == 1024)
for i := 0; i < len(msg.Data); i++ {
if msg.Data[i] != 'A' {
t.Fatalf("Invalid msg: %s", msg.Data)
}
}
}
func TestLeafNodeCompressionWithWSGetNeedsData(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
server_name: "A"
websocket {
port: -1
no_tls: true
}
leafnodes {
port: -1
compression: s2_fast
}
`))
srv1, o1 := RunServerWithConfig(conf1)
defer srv1.Shutdown()
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "B"
leafnodes {
remotes [
{
url: "ws://127.0.0.1:%d"
ws_no_masking: true
compression: s2_fast
}
]
}
`, o1.Websocket.Port)))
srv2, _ := RunServerWithConfig(conf2)
defer srv2.Shutdown()
checkLeafNodeConnected(t, srv2)
nc1 := natsConnect(t, srv1.ClientURL())
defer nc1.Close()
sub := natsSubSync(t, nc1, "foo")
checkSubInterest(t, srv2, globalAccountName, "foo", time.Second)
// We want to have the payload more than 126 bytes so that the websocket
// code need to read 2 bytes for the length. See below.
payload := "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"
sentBytes := []byte("LMSG foo 156\r\n" + payload + "\r\n")
h, _ := wsCreateFrameHeader(false, false, wsBinaryMessage, len(sentBytes))
combined := &bytes.Buffer{}
combined.Write(h)
combined.Write(sentBytes)
toSend := combined.Bytes()
// We will make a compressed block that cuts the websocket header that
// makes the reader want to read bytes directly from the connection.
// We want to make sure that we are not going to get compressed data
// without going through the (de)compress library. So for that, compress
// the first 3 bytes.
b := &bytes.Buffer{}
w := s2.NewWriter(b)
w.Write(toSend[:3])
w.Close()
var nc net.Conn
srv2.mu.RLock()
for _, l := range srv2.leafs {
l.mu.Lock()
nc = l.nc
l.mu.Unlock()
}
srv2.mu.RUnlock()
nc.Write(b.Bytes())
// Pause to make sure other side just gets a partial of the whole WS frame.
time.Sleep(100 * time.Millisecond)
b.Reset()
w.Reset(b)
w.Write(toSend[3:])
w.Close()
nc.Write(b.Bytes())
msg := natsNexMsg(t, sub, time.Second)
require_True(t, len(msg.Data) == 156)
require_Equal(t, string(msg.Data), payload)
}
func TestLeafNodeCompressionAuthTimeout(t *testing.T) {
hconf := createConfFile(t, []byte(`
port: -1
server_name: "hub"
leafnodes {
port: -1
authorization {
timeout: 0.75
}
}
`))
sh, oh := RunServerWithConfig(hconf)
defer sh.Shutdown()
sconfTmpl := `
port: -1
server_name: "%s"
cluster {
port: -1
name: "spoke"
%s
}
leafnodes {
port: -1
remotes [
{ url: "nats://127.0.0.1:%d" }
]
}
`
s1conf := createConfFile(t, []byte(fmt.Sprintf(sconfTmpl, "SP1", _EMPTY_, oh.LeafNode.Port)))
s1, o1 := RunServerWithConfig(s1conf)
defer s1.Shutdown()
s2conf := createConfFile(t, []byte(fmt.Sprintf(sconfTmpl, "SP2", fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", o1.Cluster.Port), oh.LeafNode.Port)))
s2, _ := RunServerWithConfig(s2conf)
defer s2.Shutdown()
checkClusterFormed(t, s1, s2)
checkLeafNodeConnected(t, s1)
checkLeafNodeConnected(t, s2)
getCID := func(s *Server) uint64 {
s.mu.RLock()
defer s.mu.RUnlock()
var cid uint64
for _, l := range s.leafs {
l.mu.Lock()
cid = l.cid
l.mu.Unlock()
}
return cid
}
leaf1 := getCID(s1)
leaf2 := getCID(s2)
// Wait for more than auth timeout
time.Sleep(time.Second)
checkLeafNodeConnected(t, s1)
checkLeafNodeConnected(t, s2)
if l1 := getCID(s1); l1 != leaf1 {
t.Fatalf("Leaf connection first connection had CID %v, now %v", leaf1, l1)
}
if l2 := getCID(s2); l2 != leaf2 {
t.Fatalf("Leaf connection first connection had CID %v, now %v", leaf2, l2)
}
}
func TestLeafNodeWithWeightedDQRequestsToSuperClusterWithSeparateAccounts(t *testing.T) {
sc := createJetStreamSuperClusterWithTemplate(t, jsClusterAccountsTempl, 3, 2)
defer sc.shutdown()
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, ONE and TWO.
var lnTmpl = `
listen: 127.0.0.1:-1
server_name: %s
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
{{leaf}}
cluster {
name: %s
listen: 127.0.0.1:%d
routes = [%s]
}
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
`
var leafFrag = `
leaf {
listen: 127.0.0.1:-1
remotes [
{ urls: [ %s ] }
{ urls: [ %s ] }
]
}`
// We want to have two leaf node connections that join to the same local account on the leafnode servers,
// but connect to different accounts in different clusters.
c1 := sc.clusters[0] // Will connect to account ONE
c2 := sc.clusters[1] // Will connect to account TWO
genLeafTmpl := func(tmpl string) string {
t.Helper()
var ln1, ln2 []string
for _, s := range c1.servers {
if s.ClusterName() != c1.name {
continue
}
ln := s.getOpts().LeafNode
ln1 = append(ln1, fmt.Sprintf("nats://one:p@%s:%d", ln.Host, ln.Port))
}
for _, s := range c2.servers {
if s.ClusterName() != c2.name {
continue
}
ln := s.getOpts().LeafNode
ln2 = append(ln2, fmt.Sprintf("nats://two:p@%s:%d", ln.Host, ln.Port))
}
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln1, ", "), strings.Join(ln2, ", ")), 1)
}
tmpl := strings.Replace(lnTmpl, "store_dir:", fmt.Sprintf(`domain: "%s", store_dir:`, "SA"), 1)
tmpl = genLeafTmpl(tmpl)
ln := createJetStreamCluster(t, tmpl, "SA", "SA-", 3, 22280, false)
ln.waitOnClusterReady()
defer ln.shutdown()
for _, s := range ln.servers {
checkLeafNodeConnectedCount(t, s, 2)
}
// Now connect DQ subscribers to each cluster and they separate accounts, and make sure we get the right behavior, balanced between
// them when requests originate from the leaf cluster.
// Create 5 clients for each cluster / account
var c1c, c2c []*nats.Conn
for i := 0; i < 5; i++ {
nc1, _ := jsClientConnect(t, c1.randomServer(), nats.UserInfo("one", "p"))
defer nc1.Close()
c1c = append(c1c, nc1)
nc2, _ := jsClientConnect(t, c2.randomServer(), nats.UserInfo("two", "p"))
defer nc2.Close()
c2c = append(c2c, nc2)
}
createSubs := func(num int, conns []*nats.Conn) (subs []*nats.Subscription) {
for i := 0; i < num; i++ {
nc := conns[rand.Intn(len(conns))]
sub, err := nc.QueueSubscribeSync("REQUEST", "MC")
require_NoError(t, err)
subs = append(subs, sub)
nc.Flush()
}
// Let subs propagate.
time.Sleep(100 * time.Millisecond)
return subs
}
closeSubs := func(subs []*nats.Subscription) {
for _, sub := range subs {
sub.Unsubscribe()
}
}
// Simple test first.
subs1 := createSubs(1, c1c)
defer closeSubs(subs1)
subs2 := createSubs(1, c2c)
defer closeSubs(subs2)
sendRequests := func(num int) {
t.Helper()
// Now connect to the leaf cluster and send some requests.
nc, _ := jsClientConnect(t, ln.randomServer())
defer nc.Close()
for i := 0; i < num; i++ {
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
}
nc.Flush()
}
pending := func(subs []*nats.Subscription) (total int) {
t.Helper()
for _, sub := range subs {
n, _, err := sub.Pending()
require_NoError(t, err)
total += n
}
return total
}
num := 1000
checkAllReceived := func() error {
total := pending(subs1) + pending(subs2)
if total == num {
return nil
}
return fmt.Errorf("Not all received: %d vs %d", total, num)
}
checkBalanced := func(total, pc1, pc2 int) {
t.Helper()
tf := float64(total)
e1 := tf * (float64(pc1) / 100.00)
e2 := tf * (float64(pc2) / 100.00)
delta := tf / 10
p1 := float64(pending(subs1))
if p1 < e1-delta || p1 > e1+delta {
t.Fatalf("Value out of range for subs1, expected %v got %v", e1, p1)
}
p2 := float64(pending(subs2))
if p2 < e2-delta || p2 > e2+delta {
t.Fatalf("Value out of range for subs2, expected %v got %v", e2, p2)
}
}
// Now connect to the leaf cluster and send some requests.
// Simple 50/50
sendRequests(num)
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
checkBalanced(num, 50, 50)
closeSubs(subs1)
closeSubs(subs2)
// Now test unbalanced. 10/90
subs1 = createSubs(1, c1c)
defer closeSubs(subs1)
subs2 = createSubs(9, c2c)
defer closeSubs(subs2)
sendRequests(num)
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
checkBalanced(num, 10, 90)
// Now test draining the subs as we are sending from an initial balanced situation simulating a draining of a cluster.
closeSubs(subs1)
closeSubs(subs2)
subs1, subs2 = nil, nil
// These subs slightly different.
var r1, r2 atomic.Uint64
for i := 0; i < 20; i++ {
nc := c1c[rand.Intn(len(c1c))]
sub, err := nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r1.Add(1) })
require_NoError(t, err)
subs1 = append(subs1, sub)
nc.Flush()
nc = c2c[rand.Intn(len(c2c))]
sub, err = nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r2.Add(1) })
require_NoError(t, err)
subs2 = append(subs2, sub)
nc.Flush()
}
defer closeSubs(subs1)
defer closeSubs(subs2)
nc, _ := jsClientConnect(t, ln.randomServer())
defer nc.Close()
for i, dindex := 0, 1; i < num; i++ {
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
// Check if we have more to simulate draining.
// Will drain within first ~100 requests using 20% rand test below.
// Will leave 1 behind.
if dindex < len(subs1)-1 && rand.Intn(6) > 4 {
sub := subs1[dindex]
dindex++
sub.Drain()
}
}
nc.Flush()
checkFor(t, time.Second, 200*time.Millisecond, func() error {
total := int(r1.Load() + r2.Load())
if total == num {
return nil
}
return fmt.Errorf("Not all received: %d vs %d", total, num)
})
require_True(t, r2.Load() > r1.Load())
}
func TestLeafNodeWithWeightedDQRequestsToSuperClusterWithStreamImportAccounts(t *testing.T) {
var tmpl = `
listen: 127.0.0.1:-1
server_name: %s
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
leaf { listen: 127.0.0.1:-1 }
cluster {
name: %s
listen: 127.0.0.1:%d
routes = [%s]
}
accounts {
EFG {
users = [ { user: "efg", pass: "p" } ]
jetstream: enabled
imports [
{ stream: { account: STL, subject: "REQUEST"} }
{ stream: { account: KSC, subject: "REQUEST"} }
]
}
STL {
users = [ { user: "stl", pass: "p" } ]
exports [ { stream: "REQUEST" } ]
}
KSC {
users = [ { user: "ksc", pass: "p" } ]
exports [ { stream: "REQUEST" } ]
}
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
}`
sc := createJetStreamSuperClusterWithTemplate(t, tmpl, 3, 2)
defer sc.shutdown()
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, ONE and TWO.
var lnTmpl = `
listen: 127.0.0.1:-1
server_name: %s
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
{{leaf}}
cluster {
name: %s
listen: 127.0.0.1:%d
routes = [%s]
}
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
`
var leafFrag = `
leaf {
listen: 127.0.0.1:-1
remotes [
{ urls: [ %s ] }
{ urls: [ %s ] }
{ urls: [ %s ] ; deny_export: REQUEST}
]
}`
// We want to have two leaf node connections that join to the same local account on the leafnode servers,
// but connect to different accounts in different clusters.
c1 := sc.clusters[0] // Will connect to account KSC
c2 := sc.clusters[1] // Will connect to account STL
genLeafTmpl := func(tmpl string) string {
t.Helper()
var ln1, ln2, ln3 []string
for _, s := range c1.servers {
if s.ClusterName() != c1.name {
continue
}
ln := s.getOpts().LeafNode
ln1 = append(ln1, fmt.Sprintf("nats://ksc:p@%s:%d", ln.Host, ln.Port))
}
for _, s := range c2.servers {
if s.ClusterName() != c2.name {
continue
}
ln := s.getOpts().LeafNode
ln2 = append(ln2, fmt.Sprintf("nats://stl:p@%s:%d", ln.Host, ln.Port))
ln3 = append(ln3, fmt.Sprintf("nats://efg:p@%s:%d", ln.Host, ln.Port))
}
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln1, ", "), strings.Join(ln2, ", "), strings.Join(ln3, ", ")), 1)
}
tmpl = strings.Replace(lnTmpl, "store_dir:", fmt.Sprintf(`domain: "%s", store_dir:`, "SA"), 1)
tmpl = genLeafTmpl(tmpl)
ln := createJetStreamCluster(t, tmpl, "SA", "SA-", 3, 22280, false)
ln.waitOnClusterReady()
defer ln.shutdown()
for _, s := range ln.servers {
checkLeafNodeConnectedCount(t, s, 3)
}
// Now connect DQ subscribers to each cluster but to the global account.
// Create 5 clients for each cluster / account
var c1c, c2c []*nats.Conn
for i := 0; i < 5; i++ {
nc1, _ := jsClientConnect(t, c1.randomServer(), nats.UserInfo("efg", "p"))
defer nc1.Close()
c1c = append(c1c, nc1)
nc2, _ := jsClientConnect(t, c2.randomServer(), nats.UserInfo("efg", "p"))
defer nc2.Close()
c2c = append(c2c, nc2)
}
createSubs := func(num int, conns []*nats.Conn) (subs []*nats.Subscription) {
for i := 0; i < num; i++ {
nc := conns[rand.Intn(len(conns))]
sub, err := nc.QueueSubscribeSync("REQUEST", "MC")
require_NoError(t, err)
subs = append(subs, sub)
nc.Flush()
}
// Let subs propagate.
time.Sleep(100 * time.Millisecond)
return subs
}
closeSubs := func(subs []*nats.Subscription) {
for _, sub := range subs {
sub.Unsubscribe()
}
}
// Simple test first.
subs1 := createSubs(1, c1c)
defer closeSubs(subs1)
subs2 := createSubs(1, c2c)
defer closeSubs(subs2)
sendRequests := func(num int) {
t.Helper()
// Now connect to the leaf cluster and send some requests.
nc, _ := jsClientConnect(t, ln.randomServer())
defer nc.Close()
for i := 0; i < num; i++ {
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
}
nc.Flush()
}
pending := func(subs []*nats.Subscription) (total int) {
t.Helper()
for _, sub := range subs {
n, _, err := sub.Pending()
require_NoError(t, err)
total += n
}
return total
}
num := 1000
checkAllReceived := func() error {
total := pending(subs1) + pending(subs2)
if total == num {
return nil
}
return fmt.Errorf("Not all received: %d vs %d", total, num)
}
checkBalanced := func(total, pc1, pc2 int) {
t.Helper()
tf := float64(total)
e1 := tf * (float64(pc1) / 100.00)
e2 := tf * (float64(pc2) / 100.00)
delta := tf / 10
p1 := float64(pending(subs1))
if p1 < e1-delta || p1 > e1+delta {
t.Fatalf("Value out of range for subs1, expected %v got %v", e1, p1)
}
p2 := float64(pending(subs2))
if p2 < e2-delta || p2 > e2+delta {
t.Fatalf("Value out of range for subs2, expected %v got %v", e2, p2)
}
}
// Now connect to the leaf cluster and send some requests.
// Simple 50/50
sendRequests(num)
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
checkBalanced(num, 50, 50)
closeSubs(subs1)
closeSubs(subs2)
// Now test unbalanced. 10/90
subs1 = createSubs(1, c1c)
defer closeSubs(subs1)
subs2 = createSubs(9, c2c)
defer closeSubs(subs2)
sendRequests(num)
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
checkBalanced(num, 10, 90)
closeSubs(subs1)
closeSubs(subs2)
// Now test unbalanced. 80/20
subs1 = createSubs(80, c1c)
defer closeSubs(subs1)
subs2 = createSubs(20, c2c)
defer closeSubs(subs2)
sendRequests(num)
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
checkBalanced(num, 80, 20)
// Now test draining the subs as we are sending from an initial balanced situation simulating a draining of a cluster.
closeSubs(subs1)
closeSubs(subs2)
subs1, subs2 = nil, nil
// These subs slightly different.
var r1, r2 atomic.Uint64
for i := 0; i < 20; i++ {
nc := c1c[rand.Intn(len(c1c))]
sub, err := nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r1.Add(1) })
require_NoError(t, err)
subs1 = append(subs1, sub)
nc.Flush()
nc = c2c[rand.Intn(len(c2c))]
sub, err = nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r2.Add(1) })
require_NoError(t, err)
subs2 = append(subs2, sub)
nc.Flush()
}
defer closeSubs(subs1)
defer closeSubs(subs2)
nc, _ := jsClientConnect(t, ln.randomServer())
defer nc.Close()
for i, dindex := 0, 1; i < num; i++ {
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
// Check if we have more to simulate draining.
// Will drain within first ~100 requests using 20% rand test below.
// Will leave 1 behind.
if dindex < len(subs1)-1 && rand.Intn(6) > 4 {
sub := subs1[dindex]
dindex++
sub.Drain()
}
}
nc.Flush()
checkFor(t, time.Second, 200*time.Millisecond, func() error {
total := int(r1.Load() + r2.Load())
if total == num {
return nil
}
return fmt.Errorf("Not all received: %d vs %d", total, num)
})
require_True(t, r2.Load() > r1.Load())
}
func TestLeafNodeTwoRemotesToSameHubAccount(t *testing.T) {
conf1 := createConfFile(t, []byte(`
port: -1
server_name: "hub"
accounts {
HA { users: [{user: ha, password: pwd}] }
}
leafnodes {
port: -1
}
`))
s1, o1 := RunServerWithConfig(conf1)
defer s1.Shutdown()
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: "spoke"
accounts {
A { users: [{user: A, password: pwd}] }
B { users: [{user: B, password: pwd}] }
C { users: [{user: C, password: pwd}] }
}
leafnodes {
remotes [
{
url: "nats://ha:pwd@127.0.0.1:%d"
local: "A"
}
{
url: "nats://ha:pwd@127.0.0.1:%d"
local: "C"
}
]
}
`, o1.LeafNode.Port, o1.LeafNode.Port)))
s2, _ := RunServerWithConfig(conf2)
defer s2.Shutdown()
l := &captureErrorLogger{errCh: make(chan string, 10)}
s2.SetLogger(l, false, false)
checkLeafNodeConnectedCount(t, s2, 2)
// Make sure we don't get duplicate leafnode connection errors
deadline := time.NewTimer(1500 * time.Millisecond)
for done := false; !done; {
select {
case err := <-l.errCh:
if strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
t.Fatalf("Got error: %v", err)
}
case <-deadline.C:
done = true
}
}
nca := natsConnect(t, s2.ClientURL(), nats.UserInfo("A", "pwd"))
defer nca.Close()
suba := natsSubSync(t, nca, "A")
ncb := natsConnect(t, s2.ClientURL(), nats.UserInfo("B", "pwd"))
defer ncb.Close()
subb := natsSubSync(t, ncb, "B")
ncc := natsConnect(t, s2.ClientURL(), nats.UserInfo("C", "pwd"))
defer ncc.Close()
subc := natsSubSync(t, ncc, "C")
subs := map[string]*nats.Subscription{"A": suba, "B": subb, "C": subc}
for _, subj := range []string{"A", "C"} {
checkSubInterest(t, s1, "HA", subj, time.Second)
}
nc := natsConnect(t, s1.ClientURL(), nats.UserInfo("ha", "pwd"))
defer nc.Close()
for _, subj := range []string{"A", "B", "C"} {
natsPub(t, nc, subj, []byte("hello"))
}
for _, subj := range []string{"A", "B", "C"} {
var expected bool
if subj != "B" {
expected = true
}
sub := subs[subj]
if expected {
natsNexMsg(t, sub, time.Second)
} else {
if _, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Expected timeout error, got %v", err)
}
}
}
}
func TestLeafNodeTwoRemotesToSameHubAccountWithClusters(t *testing.T) {
hubTmpl := `
port: -1
server_name: "%s"
accounts {
HA { users: [{user: HA, password: pwd}] }
}
cluster {
name: "hub"
port: -1
%s
}
leafnodes {
port: -1
}
`
confH1 := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H1", _EMPTY_)))
sh1, oh1 := RunServerWithConfig(confH1)
defer sh1.Shutdown()
confH2 := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H2", fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", oh1.Cluster.Port))))
sh2, oh2 := RunServerWithConfig(confH2)
defer sh2.Shutdown()
checkClusterFormed(t, sh1, sh2)
spokeTmpl := `
port: -1
server_name: "%s"
accounts {
A { users: [{user: A, password: pwd}] }
B { users: [{user: B, password: pwd}] }
}
cluster {
name: "spoke"
port: -1
%s
}
leafnodes {
remotes [
{
url: "nats://HA:pwd@127.0.0.1:%d"
local: "A"
}
{
url: "nats://HA:pwd@127.0.0.1:%d"
local: "B"
}
]
}
`
for _, test := range []struct {
name string
sp2Leafport int
}{
{"connect to different hub servers", oh2.LeafNode.Port},
{"connect to same hub server", oh1.LeafNode.Port},
} {
t.Run(test.name, func(t *testing.T) {
confSP1 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP1", _EMPTY_, oh1.LeafNode.Port, oh1.LeafNode.Port)))
sp1, osp1 := RunServerWithConfig(confSP1)
defer sp1.Shutdown()
confSP2 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP2",
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", osp1.Cluster.Port), test.sp2Leafport, test.sp2Leafport)))
sp2, _ := RunServerWithConfig(confSP2)
defer sp2.Shutdown()
checkClusterFormed(t, sp1, sp2)
checkLeafNodeConnectedCount(t, sp1, 2)
checkLeafNodeConnectedCount(t, sp2, 2)
var conns []*nats.Conn
createConn := func(s *Server, user string) {
t.Helper()
nc := natsConnect(t, s.ClientURL(), nats.UserInfo(user, "pwd"))
conns = append(conns, nc)
}
for _, nc := range conns {
defer nc.Close()
}
createConn(sh1, "HA")
createConn(sh2, "HA")
createConn(sp1, "A")
createConn(sp2, "A")
createConn(sp1, "B")
createConn(sp2, "B")
check := func(subConn *nats.Conn, subj string, checkA, checkB bool) {
t.Helper()
sub := natsSubSync(t, subConn, subj)
defer sub.Unsubscribe()
checkSubInterest(t, sh1, "HA", subj, time.Second)
checkSubInterest(t, sh2, "HA", subj, time.Second)
if checkA {
checkSubInterest(t, sp1, "A", subj, time.Second)
checkSubInterest(t, sp2, "A", subj, time.Second)
}
if checkB {
checkSubInterest(t, sp1, "B", subj, time.Second)
checkSubInterest(t, sp2, "B", subj, time.Second)
}
for i, ncp := range conns {
// Don't publish from account "A" connections if we are
// dealing with account "B", and vice-versa.
if !checkA && i >= 2 && i <= 3 {
continue
}
if !checkB && i >= 4 {
continue
}
natsPub(t, ncp, subj, []byte("hello"))
natsNexMsg(t, sub, time.Second)
// Make sure we don't get a duplicate
if msg, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Unexpected message or error: msg=%v - err=%v", msg, err)
}
}
}
check(conns[0], "HA.1", true, true)
check(conns[1], "HA.2", true, true)
check(conns[2], "SPA.1", true, false)
check(conns[3], "SPA.2", true, false)
check(conns[4], "SPB.1", false, true)
check(conns[5], "SPB.2", false, true)
})
}
}
func TestLeafNodeSameLocalAccountToMultipleHubs(t *testing.T) {
hub1Conf := createConfFile(t, []byte(`
port: -1
server_name: hub1
accounts {
hub1 { users: [{user: hub1, password: pwd}] }
}
leafnodes {
port: -1
}
`))
sh1, oh1 := RunServerWithConfig(hub1Conf)
defer sh1.Shutdown()
hub2Conf := createConfFile(t, []byte(`
port: -1
server_name: hub2
accounts {
hub2 { users: [{user: hub2, password: pwd}] }
}
leafnodes {
port: -1
}
`))
sh2, oh2 := RunServerWithConfig(hub2Conf)
defer sh2.Shutdown()
lconf := createConfFile(t, []byte(fmt.Sprintf(`
port: -1
server_name: leaf
accounts {
A { users: [{user: A, password: pwd}] }
B { users: [{user: B, password: pwd}] }
C { users: [{user: C, password: pwd}] }
}
leafnodes {
port: -1
remotes [
{
url: nats://hub1:pwd@127.0.0.1:%[1]d
local: "A"
}
{
url: nats://hub1:pwd@127.0.0.1:%[1]d
local: "C"
}
{
url: nats://hub2:pwd@127.0.0.1:%[2]d
local: "A"
}
{
url: nats://hub2:pwd@127.0.0.1:%[2]d
local: "B"
}
]
}
`, oh1.LeafNode.Port, oh2.LeafNode.Port)))
s, _ := RunServerWithConfig(lconf)
defer s.Shutdown()
// The leafnode to hub1 should have 2 connections (A and C)
// while the one to hub2 should have 2 connections (A and B)
checkLeafNodeConnectedCount(t, sh1, 2)
checkLeafNodeConnectedCount(t, sh2, 2)
checkLeafNodeConnectedCount(t, s, 4)
nca := natsConnect(t, s.ClientURL(), nats.UserInfo("A", "pwd"))
defer nca.Close()
suba := natsSubSync(t, nca, "A")
ncb := natsConnect(t, s.ClientURL(), nats.UserInfo("B", "pwd"))
defer ncb.Close()
subb := natsSubSync(t, ncb, "B")
ncc := natsConnect(t, s.ClientURL(), nats.UserInfo("C", "pwd"))
defer ncc.Close()
subc := natsSubSync(t, ncc, "C")
checkSubInterest(t, sh1, "hub1", "A", time.Second)
checkSubNoInterest(t, sh1, "hub1", "B", time.Second)
checkSubInterest(t, sh1, "hub1", "C", time.Second)
checkSubInterest(t, sh2, "hub2", "A", time.Second)
checkSubInterest(t, sh2, "hub2", "B", time.Second)
checkSubNoInterest(t, sh2, "hub2", "C", time.Second)
nch1 := natsConnect(t, sh1.ClientURL(), nats.UserInfo("hub1", "pwd"))
defer nch1.Close()
nch2 := natsConnect(t, sh2.ClientURL(), nats.UserInfo("hub2", "pwd"))
defer nch2.Close()
checkNoMsg := func(sub *nats.Subscription) {
t.Helper()
if msg, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
t.Fatalf("Unexpected message: %s", msg.Data)
}
}
checkSub := func(sub *nats.Subscription, subj, payload string) {
t.Helper()
msg := natsNexMsg(t, sub, time.Second)
require_Equal(t, subj, msg.Subject)
require_Equal(t, payload, string(msg.Data))
// Make sure we don't get duplicates
checkNoMsg(sub)
}
natsPub(t, nch1, "A", []byte("msgA1"))
checkSub(suba, "A", "msgA1")
natsPub(t, nch1, "B", []byte("msgB1"))
checkNoMsg(subb)
natsPub(t, nch1, "C", []byte("msgC1"))
checkSub(subc, "C", "msgC1")
natsPub(t, nch2, "A", []byte("msgA2"))
checkSub(suba, "A", "msgA2")
natsPub(t, nch2, "B", []byte("msgB2"))
checkSub(subb, "B", "msgB2")
natsPub(t, nch2, "C", []byte("msgC2"))
checkNoMsg(subc)
}