mirror of
https://github.com/hanzoai/pubsub.git
synced 2026-10-30 15:06:26 +00:00
12639 lines
338 KiB
Go
12639 lines
338 KiB
Go
// Copyright 2019-2026 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"
|
|
"compress/flate"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"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/hanzoai/pubsub/internal/fastrand"
|
|
"github.com/hanzoai/pubsub/internal/testhelper"
|
|
)
|
|
|
|
type captureLeafNodeRandomIPLogger struct {
|
|
DummyLogger
|
|
ch chan struct{}
|
|
ips [3]int
|
|
}
|
|
|
|
func (c *captureLeafNodeRandomIPLogger) Debugf(format string, v ...any) {
|
|
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.Accounts = append(o.Accounts, NewAccount("A"), NewAccount("B"))
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{NoRandomize: true, LocalAccount: "A"},
|
|
{NoRandomize: false, LocalAccount: "B"},
|
|
}
|
|
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()
|
|
|
|
var r1, r2 *leafNodeCfg
|
|
s.mu.RLock()
|
|
for r := range s.leafRemoteCfgs {
|
|
// Verify that the order of urls in the options have not been changed.
|
|
require_True(t, reflect.DeepEqual(r.URLs, orderedURLs))
|
|
// Capture the `leafNodeCfg` based on the NoRandomize option.
|
|
if r.NoRandomize {
|
|
r1 = r
|
|
} else {
|
|
r2 = r
|
|
}
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
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 ...any) {
|
|
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:%[email protected]:%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
|
|
}{
|
|
{"user creds required and no user so fails", "", "", true},
|
|
{"user creds required and pick user2 associated to ACC2", "", "user2:user2@", false},
|
|
{"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 ...any) {
|
|
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()
|
|
oa.ServerName = "A"
|
|
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()
|
|
|
|
la := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
sa.SetLogger(la, false, false)
|
|
|
|
ob := DefaultOptions()
|
|
ob.ServerName = "B"
|
|
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()
|
|
|
|
lb := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
sb.SetLogger(lb, false, false)
|
|
|
|
select {
|
|
case <-la.ch:
|
|
// OK!
|
|
case <-lb.ch:
|
|
// OK!
|
|
case <-time.After(5 * 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.connDelay = 50 * time.Millisecond
|
|
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.connDelay = 50 * time.Millisecond
|
|
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.connDelay = 50 * time.Millisecond
|
|
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 := net.JoinHostPort(opts.LeafNode.Host, fmt.Sprintf("%d", 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 TestLeafNodeInfoPermissionUpdatesRequireSolicitedLeaf(t *testing.T) {
|
|
s := &Server{}
|
|
s.setOpts(DefaultOptions())
|
|
cli, srv := net.Pipe()
|
|
defer cli.Close()
|
|
defer srv.Close()
|
|
|
|
c := &client{
|
|
kind: LEAF,
|
|
srv: s,
|
|
nc: cli,
|
|
leaf: &leaf{},
|
|
}
|
|
c.flags.set(infoReceived)
|
|
c.flags.set(compressionNegotiated)
|
|
c.flags.set(handshakeComplete)
|
|
|
|
c.processLeafnodeInfo(&Info{
|
|
Import: &SubjectPermission{Allow: []string{"import.>"}},
|
|
Export: &SubjectPermission{Allow: []string{"export.>"}},
|
|
})
|
|
|
|
require_Equal(t, c.perms, (*permissions)(nil))
|
|
}
|
|
|
|
func TestLeafNodeInfoPermissionUpdatesAllowedForSolicitedLeaf(t *testing.T) {
|
|
s := &Server{}
|
|
s.setOpts(DefaultOptions())
|
|
cli, srv := net.Pipe()
|
|
defer cli.Close()
|
|
defer srv.Close()
|
|
|
|
c := &client{
|
|
kind: LEAF,
|
|
srv: s,
|
|
nc: cli,
|
|
leaf: &leaf{
|
|
remote: &leafNodeCfg{RemoteLeafOpts: &RemoteLeafOpts{}},
|
|
},
|
|
}
|
|
c.flags.set(infoReceived)
|
|
c.flags.set(compressionNegotiated)
|
|
c.flags.set(handshakeComplete)
|
|
|
|
c.processLeafnodeInfo(&Info{
|
|
Import: &SubjectPermission{Allow: []string{"import.>"}},
|
|
Export: &SubjectPermission{Allow: []string{"export.>"}},
|
|
})
|
|
|
|
require_True(t, c.perms != nil)
|
|
require_True(t, c.pubAllowed("export.foo"))
|
|
require_True(t, c.canSubscribe("import.foo"))
|
|
require_False(t, c.pubAllowed("other.foo"))
|
|
require_False(t, c.canSubscribe("other.foo"))
|
|
}
|
|
|
|
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 j := 0; j < 100; j++ {
|
|
c.pubAllowed(nats.NewInbox())
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// The cache prune function does try for a bit to make sure the cache
|
|
// is below the maxPermCacheSize value, but depending on the machine
|
|
// this runs on, it may be that it is still a bit over. If so, run
|
|
// pubAllowed one more time and we must get below.
|
|
if n := int(atomic.LoadInt32(&c.perms.pcsz)); n > maxPermCacheSize {
|
|
c.pubAllowed(nats.NewInbox())
|
|
}
|
|
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 ...any) {
|
|
l.triggerChan <- fmt.Sprintf(format, v...)
|
|
}
|
|
|
|
func (l *chanLogger) Errorf(format string, v ...any) {
|
|
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 ...any) {
|
|
select {
|
|
case l.errCh <- fmt.Sprintf(format, v...):
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (l *oldConnReplacedLogger) Warnf(format string, v ...any) {
|
|
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 TestLeafNodeDetectDuplicateRemotesInOptions(t *testing.T) {
|
|
u, err := url.Parse("nats://user1:[email protected]:1234")
|
|
require_NoError(t, err)
|
|
urls := []*url.URL{u}
|
|
for _, test := range []struct {
|
|
name string
|
|
remote *RemoteLeafOpts
|
|
}{
|
|
{"same urls", &RemoteLeafOpts{URLs: urls}},
|
|
{"same urls/acc", &RemoteLeafOpts{URLs: urls, LocalAccount: "A"}},
|
|
{"same urls/acc/creds", &RemoteLeafOpts{URLs: urls, LocalAccount: "A", Credentials: "credsfile"}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.Accounts = append(opts.Accounts, NewAccount("A"))
|
|
opts.LeafNode.Port = -1
|
|
u, err := url.Parse("nats://user2:[email protected]:1235")
|
|
require_NoError(t, err)
|
|
rlo := &RemoteLeafOpts{URLs: []*url.URL{u}}
|
|
opts.LeafNode.Remotes = append(opts.LeafNode.Remotes, test.remote, rlo, test.remote)
|
|
_, err = NewServer(opts)
|
|
require_Error(t, err)
|
|
require_Contains(t, err.Error(), "duplicate remote")
|
|
// Ensure url is redacted
|
|
require_Contains(t, err.Error(), "user1:xxxxx")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeHubRejectDuplicateRemotes(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.ServerName = "A"
|
|
opts.LeafNode.Host = "127.0.0.1"
|
|
opts.LeafNode.Port = -1
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: "B"
|
|
leafnodes {
|
|
remotes = [{url: nats-leaf://127.0.0.1:%d}]
|
|
}
|
|
`
|
|
conf := createConfFile(t, fmt.Appendf(nil, tmpl, opts.LeafNode.Port))
|
|
ln, lno := RunServerWithConfig(conf)
|
|
defer ln.Shutdown()
|
|
|
|
// Since we correctly reject duplicate remotes, we have to manually
|
|
// add a duplicate and start soliciting here.
|
|
r := *lno.LeafNode.Remotes[0]
|
|
ln.solicitLeafNodeRemotes([]*RemoteLeafOpts{&r})
|
|
|
|
select {
|
|
case err := <-l.errCh:
|
|
if !strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
|
|
t.Fatalf("Did not get expected duplicate connection error: %v", err)
|
|
}
|
|
case <-time.After(250 * time.Millisecond):
|
|
t.Fatal("Did not get expected error")
|
|
}
|
|
}
|
|
|
|
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.Data = []byte("from srv1")
|
|
m.RespondMsg(m)
|
|
})
|
|
|
|
ncLeaf1 := natsConnect(t, leaf1.ClientURL())
|
|
defer ncLeaf1.Close()
|
|
natsQueueSub(t, ncLeaf1, "foo", "queue", func(m *nats.Msg) {
|
|
m.Data = []byte("from leaf1")
|
|
m.RespondMsg(m)
|
|
})
|
|
|
|
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
|
|
defer ncLeaf2.Close()
|
|
|
|
// Check that "foo" interest is available everywhere.
|
|
for _, s := range []*Server{srv1, leaf1, leaf2} {
|
|
gacc := s.GlobalAccount()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != 2 {
|
|
return fmt.Errorf("Expected interest for %q to be 2, got %v", "foo", n)
|
|
}
|
|
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 < 100; i++ {
|
|
// Now send the request
|
|
reqID := fmt.Sprintf("req.%d", i)
|
|
msg := nats.NewMsg("foo")
|
|
msg.Data = []byte("req")
|
|
msg.Header.Set("ReqId", reqID)
|
|
msg.Reply = sub.Subject
|
|
if err := ncLeaf2.PublishMsg(msg); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
// Check that we get the reply
|
|
replyMsg := natsNexMsg(t, sub, time.Second)
|
|
// But make sure no duplicate. We do so by checking that the reply's
|
|
// header ReqId matches our current reqID.
|
|
if respReqID := replyMsg.Header.Get("ReqId"); respReqID != reqID {
|
|
t.Fatalf("Current request is %q, got duplicate with %q", reqID, respReqID)
|
|
}
|
|
// 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 ...any) {
|
|
err := fmt.Errorf(format, v...)
|
|
select {
|
|
case l.gotErr <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (l *parseRouteLSUnsubLogger) Tracef(format string, v ...any) {
|
|
trace := fmt.Sprintf(format, v...)
|
|
if strings.Contains(trace, "LS- xyz $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 users 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 %q but got %q", 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSFIPSValidation(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
url string
|
|
}{
|
|
{
|
|
name: "no credentials",
|
|
url: "wss://127.0.0.1:1234",
|
|
},
|
|
{
|
|
name: "redacted credentials",
|
|
url: "wss://user:pass@example:7422",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
u, err := url.Parse(test.url)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
|
|
o := DefaultOptions()
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
|
|
err = validateLeafNode(o)
|
|
if wsAllowedFIPS() {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("Expected FIPS validation error, got none")
|
|
}
|
|
if strings.Contains(err.Error(), "user:pass") {
|
|
t.Fatalf("Password was not redacted: %v", err)
|
|
}
|
|
if strings.Contains(err.Error(), "user:pass@") {
|
|
t.Fatalf("Credentials were not redacted: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "FIPS-140 mode") {
|
|
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 TestLeafNodeWSSubPath(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
// Confirm that it can connect using the subpath.
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Add another leafnode that tries to connect to the subpath
|
|
// but intercept the attempt for the test.
|
|
o2 := testDefaultLeafNodeWSOptions()
|
|
lo2 := testDefaultRemoteLeafNodeWSOptions(t, o2, false)
|
|
attempts := make(chan string, 2)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts <- r.URL.String()
|
|
}))
|
|
defer ts.Close()
|
|
u, _ := url.Parse(fmt.Sprintf("%v/some/path", ts.URL))
|
|
u.Scheme = "ws"
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
},
|
|
}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
expected := "/some/path/leafnode"
|
|
select {
|
|
case got := <-attempts:
|
|
if got != expected {
|
|
t.Fatalf("Expected: %v, got: %v", expected, got)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for leaf ws connect attempt")
|
|
}
|
|
}
|
|
|
|
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, "Error soliciting websocket connection") {
|
|
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:[email protected]:%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:[email protected]:%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:[email protected]:%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:[email protected]:%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:[email protected]:%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:[email protected]:%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:[email protected]:%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 TestLeafNodeQueueGroupDistribution(t *testing.T) {
|
|
hc := createClusterWithName(t, "HUB", 3)
|
|
defer hc.shutdown()
|
|
|
|
// Now have a cluster of leafnodes with each one connecting to corresponding HUB(n) node.
|
|
c1 := `
|
|
server_name: LEAF1
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1 }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf1 := createConfFile(t, []byte(fmt.Sprintf(c1, hc.opts[0].LeafNode.Port)))
|
|
ln1, lopts1 := RunServerWithConfig(lconf1)
|
|
defer ln1.Shutdown()
|
|
|
|
c2 := `
|
|
server_name: LEAF2
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf2 := createConfFile(t, []byte(fmt.Sprintf(c2, lopts1.Cluster.Port, hc.opts[1].LeafNode.Port)))
|
|
ln2, _ := RunServerWithConfig(lconf2)
|
|
defer ln2.Shutdown()
|
|
|
|
c3 := `
|
|
server_name: LEAF3
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf3 := createConfFile(t, []byte(fmt.Sprintf(c3, lopts1.Cluster.Port, hc.opts[2].LeafNode.Port)))
|
|
ln3, _ := RunServerWithConfig(lconf3)
|
|
defer ln3.Shutdown()
|
|
|
|
// Check leaf cluster is formed and all connected to the HUB.
|
|
lnServers := []*Server{ln1, ln2, ln3}
|
|
checkClusterFormed(t, lnServers...)
|
|
for _, s := range lnServers {
|
|
checkLeafNodeConnected(t, s)
|
|
}
|
|
// Check each node in the hub has 1 connection from the leaf cluster.
|
|
for i := 0; i < 3; i++ {
|
|
checkLeafNodeConnectedCount(t, hc.servers[i], 1)
|
|
}
|
|
|
|
// Create a client and qsub on LEAF1 and LEAF2.
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
var qsub1Count atomic.Int32
|
|
natsQueueSub(t, nc1, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub1Count.Add(1)
|
|
})
|
|
natsFlush(t, nc1)
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
var qsub2Count atomic.Int32
|
|
natsQueueSub(t, nc2, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub2Count.Add(1)
|
|
})
|
|
natsFlush(t, nc2)
|
|
|
|
// Make sure that the propagation interest is done before sending.
|
|
for _, s := range hc.servers {
|
|
gacc := s.GlobalAccount()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != 2 {
|
|
return fmt.Errorf("Expected interest for %q to be 2, got %v", "foo", n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
sendAndCheck := func(idx int) {
|
|
t.Helper()
|
|
nchub := natsConnect(t, hc.servers[idx].ClientURL())
|
|
defer nchub.Close()
|
|
total := 1000
|
|
for i := 0; i < total; i++ {
|
|
natsPub(t, nchub, "foo", []byte("from hub"))
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if trecv := int(qsub1Count.Load() + qsub2Count.Load()); trecv != total {
|
|
return fmt.Errorf("Expected %v messages, got %v", total, trecv)
|
|
}
|
|
return nil
|
|
})
|
|
// Now that we have made sure that all messages were received,
|
|
// check that qsub1 and qsub2 are getting at least some.
|
|
if n := int(qsub1Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub1 to get some messages, but got %v", n)
|
|
}
|
|
if n := int(qsub2Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub2 to get some messages, but got %v", n)
|
|
}
|
|
// Reset the counters.
|
|
qsub1Count.Store(0)
|
|
qsub2Count.Store(0)
|
|
}
|
|
// Send from HUB1
|
|
sendAndCheck(0)
|
|
// Send from HUB2
|
|
sendAndCheck(1)
|
|
// Send from HUB3
|
|
sendAndCheck(2)
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupDistributionVariant(t *testing.T) {
|
|
hc := createClusterWithName(t, "HUB", 3)
|
|
defer hc.shutdown()
|
|
|
|
// Now have a cluster of leafnodes with LEAF1 and LEAF2 connecting to HUB1.
|
|
c1 := `
|
|
server_name: LEAF1
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1 }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf1 := createConfFile(t, []byte(fmt.Sprintf(c1, hc.opts[0].LeafNode.Port)))
|
|
ln1, lopts1 := RunServerWithConfig(lconf1)
|
|
defer ln1.Shutdown()
|
|
|
|
c2 := `
|
|
server_name: LEAF2
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf2 := createConfFile(t, []byte(fmt.Sprintf(c2, lopts1.Cluster.Port, hc.opts[0].LeafNode.Port)))
|
|
ln2, _ := RunServerWithConfig(lconf2)
|
|
defer ln2.Shutdown()
|
|
|
|
// And LEAF3 to HUB3
|
|
c3 := `
|
|
server_name: LEAF3
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf3 := createConfFile(t, []byte(fmt.Sprintf(c3, lopts1.Cluster.Port, hc.opts[2].LeafNode.Port)))
|
|
ln3, _ := RunServerWithConfig(lconf3)
|
|
defer ln3.Shutdown()
|
|
|
|
// Check leaf cluster is formed and all connected to the HUB.
|
|
lnServers := []*Server{ln1, ln2, ln3}
|
|
checkClusterFormed(t, lnServers...)
|
|
for _, s := range lnServers {
|
|
checkLeafNodeConnected(t, s)
|
|
}
|
|
// Check that HUB1 has 2 leaf connections, HUB2 has 0 and HUB3 has 1.
|
|
checkLeafNodeConnectedCount(t, hc.servers[0], 2)
|
|
checkLeafNodeConnectedCount(t, hc.servers[1], 0)
|
|
checkLeafNodeConnectedCount(t, hc.servers[2], 1)
|
|
|
|
// Create a client and qsub on LEAF1 and LEAF2.
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
var qsub1Count atomic.Int32
|
|
natsQueueSub(t, nc1, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub1Count.Add(1)
|
|
})
|
|
natsFlush(t, nc1)
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
var qsub2Count atomic.Int32
|
|
natsQueueSub(t, nc2, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub2Count.Add(1)
|
|
})
|
|
natsFlush(t, nc2)
|
|
|
|
// Make sure that the propagation interest is done before sending.
|
|
for i, s := range hc.servers {
|
|
gacc := s.GlobalAccount()
|
|
var ei int
|
|
switch i {
|
|
case 0:
|
|
ei = 2
|
|
default:
|
|
ei = 1
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != ei {
|
|
return fmt.Errorf("Expected interest for %q to be %d, got %v", "foo", ei, n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
sendAndCheck := func(idx int) {
|
|
t.Helper()
|
|
nchub := natsConnect(t, hc.servers[idx].ClientURL())
|
|
defer nchub.Close()
|
|
total := 1000
|
|
for i := 0; i < total; i++ {
|
|
natsPub(t, nchub, "foo", []byte("from hub"))
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if trecv := int(qsub1Count.Load() + qsub2Count.Load()); trecv != total {
|
|
return fmt.Errorf("Expected %v messages, got %v", total, trecv)
|
|
}
|
|
return nil
|
|
})
|
|
// Now that we have made sure that all messages were received,
|
|
// check that qsub1 and qsub2 are getting at least some.
|
|
if n := int(qsub1Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub1 to get some messages, but got %v (qsub2=%v)", n, qsub2Count.Load())
|
|
}
|
|
if n := int(qsub2Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub2 to get some messages, but got %v (qsub1=%v)", n, qsub1Count.Load())
|
|
}
|
|
// Reset the counters.
|
|
qsub1Count.Store(0)
|
|
qsub2Count.Store(0)
|
|
}
|
|
// Send from HUB1
|
|
sendAndCheck(0)
|
|
// Send from HUB2
|
|
sendAndCheck(1)
|
|
// Send from HUB3
|
|
sendAndCheck(2)
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupDistributionWithDaisyChainAndGateway(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
// We create a sort of a ladder of servers with connections that look like this:
|
|
//
|
|
// D1 <--- route ---> D2
|
|
// | |
|
|
// GW GW
|
|
// | |
|
|
// C1 <--- route ---> C2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// B1 <--- route ---> B2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// A1 <--- route ---> A2
|
|
//
|
|
// We will then place queue subscriptions (different sub-tests) on A1, A2
|
|
// B1, B2, D1 and D2.
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
dConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: "D"
|
|
port: -1
|
|
%s
|
|
}
|
|
gateway {
|
|
name: "D"
|
|
port: -1
|
|
}
|
|
`
|
|
d1Conf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, "GW1", _EMPTY_)))
|
|
d1, d1Opts := RunServerWithConfig(d1Conf)
|
|
defer d1.Shutdown()
|
|
|
|
d2Conf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, "GW2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", d1Opts.Cluster.Port))))
|
|
d2, d2Opts := RunServerWithConfig(d2Conf)
|
|
defer d2.Shutdown()
|
|
|
|
checkClusterFormed(t, d1, d2)
|
|
|
|
leafCConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: C
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
gateway {
|
|
name: C
|
|
port: -1
|
|
gateways [
|
|
{
|
|
name: D
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
c1Conf := createConfFile(t, []byte(fmt.Sprintf(leafCConf, accs, "C1", _EMPTY_, d1Opts.Gateway.Port)))
|
|
c1, c1Opts := RunServerWithConfig(c1Conf)
|
|
defer c1.Shutdown()
|
|
|
|
waitForOutboundGateways(t, c1, 1, time.Second)
|
|
waitForInboundGateways(t, d1, 1, time.Second)
|
|
|
|
c2Conf := createConfFile(t, []byte(fmt.Sprintf(leafCConf, accs, "C2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", c1Opts.Cluster.Port), d2Opts.Gateway.Port)))
|
|
c2, c2Opts := RunServerWithConfig(c2Conf)
|
|
defer c2.Shutdown()
|
|
|
|
waitForOutboundGateways(t, c2, 1, time.Second)
|
|
waitForInboundGateways(t, d2, 1, time.Second)
|
|
|
|
checkClusterFormed(t, c1, c2)
|
|
|
|
leafABConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: %s
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "B1", "B", _EMPTY_, c1Opts.LeafNode.Port)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, c1)
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "B2", "B",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), c2Opts.LeafNode.Port)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, c2)
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "A1", "A", _EMPTY_, b1Opts.LeafNode.Port)))
|
|
a1, a1Opts := RunServerWithConfig(a1Conf)
|
|
defer a1.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, b1, 2)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "A2", "A",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", a1Opts.Cluster.Port), b2Opts.LeafNode.Port)))
|
|
a2, _ := RunServerWithConfig(a2Conf)
|
|
defer a2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, b2, 2)
|
|
checkLeafNodeConnected(t, a2)
|
|
checkClusterFormed(t, a1, a2)
|
|
|
|
// Create our client connections to all servers where we may need to have
|
|
// queue subscriptions.
|
|
ncD1 := natsConnect(t, d1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncD1.Close()
|
|
ncD2 := natsConnect(t, d2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncD2.Close()
|
|
ncB1 := natsConnect(t, b1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB1.Close()
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
ncA1 := natsConnect(t, a1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA1.Close()
|
|
ncA2 := natsConnect(t, a2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
|
|
// Helper to check that the interest is propagated to all servers
|
|
checkInterest := func(t *testing.T, subj string) {
|
|
t.Helper()
|
|
for _, s := range []*Server{a1, a2, b1, b2, c1, c2, d1, d2} {
|
|
acc, err := s.LookupAccount("USER")
|
|
require_NoError(t, err)
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
if acc.Interest(subj) != 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Still no interest on %q in server %q", subj, s)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper to send messages on given subject. We are always sending
|
|
// from cluster B in this test, but we pick randomly between B1 and B2.
|
|
total := 1000
|
|
send := func(t *testing.T, subj string) {
|
|
for i := 0; i < total; i++ {
|
|
var nc *nats.Conn
|
|
if fastrand.Uint32n(2) == 0 {
|
|
nc = ncB1
|
|
} else {
|
|
nc = ncB2
|
|
}
|
|
natsPub(t, nc, subj, []byte(fmt.Sprintf("msg_%d", i+1)))
|
|
}
|
|
}
|
|
|
|
const queue = "queue"
|
|
|
|
for i, test := range []struct {
|
|
name string
|
|
a1 bool
|
|
a2 bool
|
|
b1 bool
|
|
b2 bool
|
|
d1 bool
|
|
d2 bool
|
|
}{
|
|
// Cases with QSubs in A, B and D
|
|
{"A1 __ B1 __ D1 __", true, false, true, false, true, false},
|
|
{"A1 __ B1 __ __ D2", true, false, true, false, false, true},
|
|
{"A1 __ B1 __ D1 D2", true, false, true, false, true, true},
|
|
|
|
{"A1 __ __ B2 D1 __", true, false, false, true, true, false},
|
|
{"A1 __ __ B2 __ D2", true, false, false, true, false, true},
|
|
{"A1 __ __ B2 D1 D2", true, false, false, true, true, true},
|
|
|
|
{"A1 __ B1 B2 D1 __", true, false, true, true, true, false},
|
|
{"A1 __ B1 B2 __ D2", true, false, true, true, false, true},
|
|
{"A1 __ B1 B2 D1 D2", true, false, true, true, true, true},
|
|
|
|
{"__ A2 B1 __ D1 __", false, true, true, false, true, false},
|
|
{"__ A2 B1 __ __ D2", false, true, true, false, false, true},
|
|
{"__ A2 B1 __ D1 D2", false, true, true, false, true, true},
|
|
|
|
{"__ A2 __ B2 D1 __", false, true, false, true, true, false},
|
|
{"__ A2 __ B2 __ D2", false, true, false, true, false, true},
|
|
{"__ A2 __ B2 D1 D2", false, true, false, true, true, true},
|
|
|
|
{"__ A2 B1 B2 D1 __", false, true, true, true, true, false},
|
|
{"__ A2 B1 B2 __ D2", false, true, true, true, false, true},
|
|
{"__ A2 B1 B2 D1 D2", false, true, true, true, true, true},
|
|
|
|
{"A1 A2 B1 __ D1 __", true, true, true, false, true, false},
|
|
{"A1 A2 B1 __ __ D2", true, true, true, false, false, true},
|
|
{"A1 A2 B1 __ D1 D2", true, true, true, false, true, true},
|
|
|
|
{"A1 A2 __ B2 D1 __", true, true, false, true, true, false},
|
|
{"A1 A2 __ B2 __ D2", true, true, false, true, false, true},
|
|
{"A1 A2 __ B2 D1 D2", true, true, false, true, true, true},
|
|
|
|
{"A1 A2 B1 B2 D1 __", true, true, true, true, true, false},
|
|
{"A1 A2 B1 B2 __ D2", true, true, true, true, false, true},
|
|
{"A1 A2 B1 B2 D1 D2", true, true, true, true, true, true},
|
|
|
|
// Now without any QSub in B cluster (so just A and D)
|
|
{"A1 __ __ __ D1 __", true, false, false, false, true, false},
|
|
{"A1 __ __ __ __ D2", true, false, false, false, false, true},
|
|
{"A1 __ __ __ D1 D2", true, false, false, false, true, true},
|
|
|
|
{"__ A2 __ __ D1 __", false, true, false, false, true, false},
|
|
{"__ A2 __ __ __ D2", false, true, false, false, false, true},
|
|
{"__ A2 __ __ D1 D2", false, true, false, false, true, true},
|
|
|
|
{"A1 A2 __ __ D1 __", true, true, false, false, true, false},
|
|
{"A1 A2 __ __ __ D2", true, true, false, false, false, true},
|
|
{"A1 A2 __ __ D1 D2", true, true, false, false, true, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
subj := fmt.Sprintf("foo.%d", i+1)
|
|
var aCount, bCount, dCount atomic.Int32
|
|
if test.a1 {
|
|
qsA1 := natsQueueSub(t, ncA1, subj, queue, func(_ *nats.Msg) {
|
|
aCount.Add(1)
|
|
})
|
|
defer qsA1.Unsubscribe()
|
|
}
|
|
if test.a2 {
|
|
qsA2 := natsQueueSub(t, ncA2, subj, queue, func(_ *nats.Msg) {
|
|
aCount.Add(1)
|
|
})
|
|
defer qsA2.Unsubscribe()
|
|
}
|
|
if test.b1 {
|
|
qsB1 := natsQueueSub(t, ncB1, subj, queue, func(_ *nats.Msg) {
|
|
bCount.Add(1)
|
|
})
|
|
defer qsB1.Unsubscribe()
|
|
}
|
|
if test.b2 {
|
|
qsB2 := natsQueueSub(t, ncB2, subj, queue, func(_ *nats.Msg) {
|
|
bCount.Add(1)
|
|
})
|
|
defer qsB2.Unsubscribe()
|
|
}
|
|
if test.d1 {
|
|
qsD1 := natsQueueSub(t, ncD1, subj, queue, func(_ *nats.Msg) {
|
|
dCount.Add(1)
|
|
})
|
|
defer qsD1.Unsubscribe()
|
|
}
|
|
if test.d2 {
|
|
qsD2 := natsQueueSub(t, ncD2, subj, queue, func(_ *nats.Msg) {
|
|
dCount.Add(1)
|
|
})
|
|
defer qsD2.Unsubscribe()
|
|
}
|
|
checkInterest(t, subj)
|
|
|
|
// Now send messages
|
|
send(t, subj)
|
|
|
|
// Check that appropriate queue subs receive all messages.
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
n := aCount.Load() + bCount.Load() + dCount.Load()
|
|
if n == int32(total) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Got only %v/%v messages (a=%v b=%v d=%v)", n, total, aCount.Load(), bCount.Load(), dCount.Load())
|
|
})
|
|
// When there is (are) qsub(s) on b, then only B should
|
|
// get the messages. Otherwise, it should be between A and D
|
|
if test.b1 || test.b2 {
|
|
require_Equal(t, aCount.Load(), 0)
|
|
require_Equal(t, dCount.Load(), 0)
|
|
} else {
|
|
require_Equal(t, bCount.Load(), 0)
|
|
// We should have receive some on A and D
|
|
require_True(t, aCount.Load() > 0)
|
|
require_True(t, dCount.Load() > 0)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeAndGatewaysSingleMsgPerQueueGroup(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
gwUSConfTmpl := `
|
|
%s
|
|
server_name: GW_US
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: US
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
gwUSConf := createConfFile(t, []byte(fmt.Sprintf(gwUSConfTmpl, accs)))
|
|
gwUS, gwUSOpts := RunServerWithConfig(gwUSConf)
|
|
defer gwUS.Shutdown()
|
|
|
|
gwEUConfTmpl := `
|
|
%s
|
|
server_name: GW_EU
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: EU
|
|
listen: "127.0.0.1:-1"
|
|
gateways [
|
|
{
|
|
name: US
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
gwEUConf := createConfFile(t, []byte(fmt.Sprintf(gwEUConfTmpl, accs, gwUSOpts.Gateway.Port)))
|
|
gwEU, gwEUOpts := RunServerWithConfig(gwEUConf)
|
|
defer gwEU.Shutdown()
|
|
|
|
waitForOutboundGateways(t, gwUS, 1, time.Second)
|
|
waitForOutboundGateways(t, gwEU, 1, time.Second)
|
|
waitForInboundGateways(t, gwUS, 1, time.Second)
|
|
waitForInboundGateways(t, gwEU, 1, time.Second)
|
|
|
|
leafConfTmpl := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
leafUSConf := createConfFile(t, []byte(fmt.Sprintf(leafConfTmpl, accs, "LEAF_US", gwUSOpts.LeafNode.Port)))
|
|
leafUS, _ := RunServerWithConfig(leafUSConf)
|
|
defer leafUS.Shutdown()
|
|
checkLeafNodeConnected(t, leafUS)
|
|
|
|
leafEUConf := createConfFile(t, []byte(fmt.Sprintf(leafConfTmpl, accs, "LEAF_EU", gwEUOpts.LeafNode.Port)))
|
|
leafEU, _ := RunServerWithConfig(leafEUConf)
|
|
defer leafEU.Shutdown()
|
|
checkLeafNodeConnected(t, leafEU)
|
|
|
|
// Order is important! (see rest of test to understand why)
|
|
var usLeafQ, usLeafPS, usGWQ, usGWPS, euGWQ, euGWPS, euLeafQ, euLeafPS, euLeafQBaz atomic.Int32
|
|
counters := []*atomic.Int32{&usLeafQ, &usLeafPS, &usGWQ, &usGWPS, &euGWQ, &euGWPS, &euLeafQ, &euLeafPS, &euLeafQBaz}
|
|
counterNames := []string{"usLeafQ", "usLeafPS", "usGWQ", "usGWPS", "euGWQ", "euGWPS", "euLeafQ", "euLeafPS", "euLeafQBaz"}
|
|
if len(counters) != len(counterNames) {
|
|
panic("Fix test!")
|
|
}
|
|
resetCounters := func() {
|
|
for _, a := range counters {
|
|
a.Store(0)
|
|
}
|
|
}
|
|
|
|
// This test will always produce from the US leaf.
|
|
ncProd := natsConnect(t, leafUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncProd.Close()
|
|
|
|
total := int32(1)
|
|
check := func(t *testing.T, expected []int32) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
resetCounters()
|
|
for i := 0; i < int(total); i++ {
|
|
natsPub(t, ncProd, "foo.1", []byte("hello"))
|
|
}
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
for i := 0; i < len(expected); i++ {
|
|
if n := counters[i].Load(); n != expected[i] {
|
|
return fmt.Errorf("Expected counter %q to be %v, got %v", counterNames[i], expected[i], n)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// "usLeafQ", "usLeafPS", "usGWQ", "usGWPS", "euGWQ", "euGWPS", "euLeafQ", "euLeafPS", "euLeafQBaz"
|
|
for _, test := range []struct {
|
|
subs []int
|
|
expected []int32
|
|
}{
|
|
// We will always have the qsub on leaf EU, and have some permutations
|
|
// of queue and plain subs on other server(s) and check we get the
|
|
// expected distribution.
|
|
|
|
// Simple test firs, qsubs on leaf US and leaf EU, all messages stay in leaf US.
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
// Move the queue sub from leaf US to GW US.
|
|
{
|
|
[]int{0, 0, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{0, 0, total, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
// Now move it to GW EU.
|
|
{
|
|
[]int{0, 0, 0, 0, 1, 0, 1, 0, 0},
|
|
[]int32{0, 0, 0, 0, total, 0, 0, 0, 0},
|
|
},
|
|
|
|
// More combinations...
|
|
{
|
|
[]int{1, 1, 0, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{0, total, total, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 1, 1, 0, 0, 1, 0, 0},
|
|
[]int32{0, total, total, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 0, 1, 1, 0, 1, 0, 0},
|
|
[]int32{0, total, 0, total, total, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 0, 1, 1, 1, 1, 0, 0},
|
|
[]int32{0, total, 0, total, total, total, 0, 0, 0},
|
|
},
|
|
// If we have the qsub in leaf US, does not matter if we have
|
|
// qsubs in GW US and EU, only leaf US should receive the messages,
|
|
// but plain sub in GW servers should get them too.
|
|
{
|
|
[]int{1, 1, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 1, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, total, 0, 0, 0},
|
|
},
|
|
// Now back to a qsub on leaf US and leaf EU, but introduce plain sub
|
|
// interest in leaf EU
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 1, 0},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, total, 0},
|
|
},
|
|
// And add a different queue group in leaf EU and it should get the messages too.
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
// Keep plain and baz queue sub interests in leaf EU and add more combinations.
|
|
{
|
|
[]int{1, 1, 0, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 1, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, total, 0, total, total},
|
|
},
|
|
} {
|
|
t.Run(_EMPTY_, func(t *testing.T) {
|
|
if len(test.subs) != len(counters) || len(test.expected) != len(counters) {
|
|
panic("Fix test")
|
|
}
|
|
|
|
ncUS := natsConnect(t, leafUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncUS.Close()
|
|
|
|
ncGWUS := natsConnect(t, gwUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncGWUS.Close()
|
|
|
|
ncGWEU := natsConnect(t, gwEU.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncGWEU.Close()
|
|
|
|
ncEU := natsConnect(t, leafEU.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncEU.Close()
|
|
|
|
if test.subs[0] > 0 {
|
|
natsQueueSub(t, ncUS, "foo.*", "bar", func(_ *nats.Msg) {
|
|
usLeafQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[1] > 0 {
|
|
natsSub(t, ncUS, "foo.>", func(_ *nats.Msg) {
|
|
usLeafPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncUS)
|
|
if test.subs[2] > 0 {
|
|
natsQueueSub(t, ncGWUS, "foo.*", "bar", func(_ *nats.Msg) {
|
|
usGWQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[3] > 0 {
|
|
natsSub(t, ncGWUS, "foo.>", func(_ *nats.Msg) {
|
|
usGWPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncGWUS)
|
|
if test.subs[4] > 0 {
|
|
natsQueueSub(t, ncGWEU, "foo.*", "bar", func(_ *nats.Msg) {
|
|
euGWQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[5] > 0 {
|
|
natsSub(t, ncGWEU, "foo.>", func(_ *nats.Msg) {
|
|
euGWPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncGWEU)
|
|
if test.subs[6] > 0 {
|
|
natsQueueSub(t, ncEU, "foo.*", "bar", func(_ *nats.Msg) {
|
|
euLeafQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[7] > 0 {
|
|
natsSub(t, ncEU, "foo.>", func(_ *nats.Msg) {
|
|
euLeafPS.Add(1)
|
|
})
|
|
}
|
|
if test.subs[8] > 0 {
|
|
// Create on different group, called "baz"
|
|
natsQueueSub(t, ncEU, "foo.*", "baz", func(_ *nats.Msg) {
|
|
euLeafQBaz.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncEU)
|
|
|
|
// Check that we have what we expect.
|
|
check(t, test.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupWeightCorrectOnConnectionCloseInSuperCluster(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
//
|
|
// D
|
|
// |
|
|
// Leaf
|
|
// |
|
|
// v
|
|
// C
|
|
// ^ ^
|
|
// / \
|
|
// GW GW
|
|
// / \
|
|
// v \
|
|
// B1 <--- route ---> B2 <----*----------*
|
|
// ^ <---* | |
|
|
// | | Leaf Leaf
|
|
// Leaf *-- Leaf ---* | |
|
|
// | | | |
|
|
// A1 <--- route ---> A2 OTHER1 OTHER2
|
|
//
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
bConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "B"
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
%s
|
|
}
|
|
gateway {
|
|
name: "B"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
sb1Conf := createConfFile(t, []byte(fmt.Sprintf(bConf, accs, "B1", _EMPTY_)))
|
|
sb1, sb1o := RunServerWithConfig(sb1Conf)
|
|
defer sb1.Shutdown()
|
|
|
|
sb2Conf := createConfFile(t, []byte(fmt.Sprintf(bConf, accs, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", sb1o.Cluster.Port))))
|
|
sb2, sb2o := RunServerWithConfig(sb2Conf)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
|
|
cConf := `
|
|
%s
|
|
server_name: C
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "C"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
gateway {
|
|
name: "C"
|
|
listen: "127.0.0.1:-1"
|
|
gateways [
|
|
{
|
|
name: B
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
scConf := createConfFile(t, []byte(fmt.Sprintf(cConf, accs, sb1o.Gateway.Port)))
|
|
sc, sco := RunServerWithConfig(scConf)
|
|
defer sc.Shutdown()
|
|
|
|
waitForOutboundGateways(t, sc, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, sb1, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, sb2, 1, 2*time.Second)
|
|
waitForInboundGateways(t, sc, 2, 2*time.Second)
|
|
waitForInboundGateways(t, sb1, 1, 2*time.Second)
|
|
|
|
dConf := `
|
|
%s
|
|
server_name: D
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "D"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
sdConf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, sco.LeafNode.Port)))
|
|
sd, _ := RunServerWithConfig(sdConf)
|
|
defer sd.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sc)
|
|
checkLeafNodeConnected(t, sd)
|
|
|
|
aConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: A
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
%s
|
|
}
|
|
leafnodes {
|
|
reconnect: "10ms"
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(aConf, accs, "A1", _EMPTY_, sb1o.LeafNode.Port)))
|
|
sa1, sa1o := RunServerWithConfig(a1Conf)
|
|
defer sa1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa1)
|
|
checkLeafNodeConnected(t, sb1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(aConf, accs, "A2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", sa1o.Cluster.Port), sb1o.LeafNode.Port)))
|
|
sa2, _ := RunServerWithConfig(a2Conf)
|
|
defer sa2.Shutdown()
|
|
|
|
checkClusterFormed(t, sa1, sa2)
|
|
checkLeafNodeConnected(t, sa2)
|
|
checkLeafNodeConnectedCount(t, sb1, 2)
|
|
|
|
otherLeafsConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
o1Conf := createConfFile(t, []byte(fmt.Sprintf(otherLeafsConf, accs, "OTHERLEAF1", sb2o.LeafNode.Port)))
|
|
so1, _ := RunServerWithConfig(o1Conf)
|
|
defer so1.Shutdown()
|
|
checkLeafNodeConnected(t, so1)
|
|
checkLeafNodeConnectedCount(t, sb2, 1)
|
|
|
|
o2Conf := createConfFile(t, []byte(fmt.Sprintf(otherLeafsConf, accs, "OTHERLEAF2", sb2o.LeafNode.Port)))
|
|
so2, _ := RunServerWithConfig(o2Conf)
|
|
defer so2.Shutdown()
|
|
checkLeafNodeConnected(t, so2)
|
|
checkLeafNodeConnectedCount(t, sb2, 2)
|
|
|
|
// Helper to check that the interest is propagated to all servers
|
|
checkInterest := func(t *testing.T, expected []int, expectedGW int32) {
|
|
t.Helper()
|
|
subj := "foo"
|
|
for i, s := range []*Server{sa1, sa2, so1, so2, sb1, sb2, sc, sd} {
|
|
if s == sc || !s.isRunning() {
|
|
continue
|
|
}
|
|
acc, err := s.LookupAccount("USER")
|
|
require_NoError(t, err)
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
n := acc.Interest(subj)
|
|
if n == expected[i] {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Expected interest count for server %q to be %v, got %v", s, expected[i], n)
|
|
})
|
|
}
|
|
// For server C, need to check in gateway's account.
|
|
checkForRegisteredQSubInterest(t, sc, "B", "USER", "foo", expected[6], time.Second)
|
|
|
|
// For server B1 and B2, check that we have the proper counts in the map.
|
|
for _, s := range []*Server{sb1, sb2} {
|
|
if !s.isRunning() {
|
|
continue
|
|
}
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
s.gateway.pasi.Lock()
|
|
accMap := s.gateway.pasi.m
|
|
st := accMap["USER"]
|
|
var n int32
|
|
entry, ok := st["foo bar"]
|
|
if ok {
|
|
n = entry.n
|
|
}
|
|
s.gateway.pasi.Unlock()
|
|
if n == expectedGW {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Expected GW interest count for server %q to be %v, got %v", s, expectedGW, n)
|
|
})
|
|
}
|
|
}
|
|
|
|
ncA1 := natsConnect(t, sa1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA1.Close()
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSubSync(t, ncA1, "foo", "bar")
|
|
}
|
|
natsFlush(t, ncA1)
|
|
// With 3 queue subs on A1, we should have for servers (in order checked in checkInterest)
|
|
// for A1: 3 locals, for all others, 1 for the remote sub from A1.
|
|
// B1 and B2 GW map will be 3 (1 for each sub)
|
|
checkInterest(t, []int{3, 1, 1, 1, 1, 1, 1, 1}, 3)
|
|
|
|
ncA2 := natsConnect(t, sa2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
ncA2qsub1 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
ncA2qsub2 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
// A1 will have 1 more for remote sub, same for A2 (2 locals + 1 remote).
|
|
// B1 will have 2 interest (1 per leaf connection)
|
|
// B1 and B2 GW map goes to 5.
|
|
checkInterest(t, []int{4, 3, 1, 1, 2, 1, 1, 1}, 5)
|
|
|
|
ncOther1 := natsConnect(t, so1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncOther1.Close()
|
|
natsQueueSubSync(t, ncOther1, "foo", "bar")
|
|
natsQueueSubSync(t, ncOther1, "foo", "bar")
|
|
natsFlush(t, ncOther1)
|
|
// A1, A2 will have one more because of routed interest
|
|
// O1 will have 3 (2 locals + 1 for remote interest)
|
|
// O2 has still 1 for remote interest
|
|
// B1 has 1 more because of new leaf interest and B2 because of routed interest.
|
|
// B1 and B2 GW map goes to 7.
|
|
checkInterest(t, []int{5, 4, 3, 1, 3, 2, 1, 1}, 7)
|
|
|
|
ncOther2 := natsConnect(t, so2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncOther2.Close()
|
|
natsQueueSubSync(t, ncOther2, "foo", "bar")
|
|
natsFlush(t, ncOther2)
|
|
// O2 1 more for local interest
|
|
// B2 1 more for the new leaf interest
|
|
// B1 and B2 GW map goes to 8.
|
|
checkInterest(t, []int{5, 4, 3, 2, 3, 3, 1, 1}, 8)
|
|
|
|
// Stop the server so1.
|
|
so1.Shutdown()
|
|
so1.WaitForShutdown()
|
|
checkLeafNodeConnectedCount(t, sb2, 1)
|
|
// Now check interest still valid, but wait a little bit to make sure that
|
|
// even with the bug where we would send an RS- through the gateway, there
|
|
// would be enough time for it to propagate before we check for interest.
|
|
time.Sleep(250 * time.Millisecond)
|
|
// O1 is stopped, so expect 0
|
|
// B2 has 1 less because leaf connection went away.
|
|
// B1 and B2 GW map goes down to 6.
|
|
checkInterest(t, []int{5, 4, 0, 2, 3, 2, 1, 1}, 6)
|
|
|
|
// Store server sa1.
|
|
sa1.Shutdown()
|
|
sa1.WaitForShutdown()
|
|
checkLeafNodeConnectedCount(t, sb1, 1)
|
|
time.Sleep(250 * time.Millisecond)
|
|
// A1 and O1 are gone, so 0
|
|
// A2 has 1 less due to loss of routed interest
|
|
// B1 has 1 less because 1 leaf connection went away.
|
|
// B1 and B2 GW map goes down to 3.
|
|
checkInterest(t, []int{0, 3, 0, 2, 2, 2, 1, 1}, 3)
|
|
|
|
// Now remove the queue subs from A2
|
|
ncA2qsub1.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
// A2 has 1 less
|
|
checkInterest(t, []int{0, 2, 0, 2, 2, 2, 1, 1}, 2)
|
|
|
|
ncA2qsub2.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
// A2 has 1 (no more locals but still interest for O2).
|
|
// O2 has 1 (no more for remote interest, only local).
|
|
// B1, B2 has 1 less since no interest from any of its leaf connections.
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
// Removing (closing connection) of the sub on O2 will remove
|
|
// interest globally.
|
|
ncOther2.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Resubscribe now, and again, interest should be propagated.
|
|
natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 1, 1, 1}, 2)
|
|
|
|
// Close the client connection that has the 2 queue subs.
|
|
ncA2.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Now we will test when a route is lost on a server that has gateway enabled
|
|
// that we update counts properly.
|
|
ncB2 := natsConnect(t, sb2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsFlush(t, ncB2)
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 3, 1, 1}, 3)
|
|
|
|
ncB1 := natsConnect(t, sb1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB1.Close()
|
|
natsQueueSubSync(t, ncB1, "foo", "bar")
|
|
natsQueueSubSync(t, ncB1, "foo", "bar")
|
|
checkInterest(t, []int{0, 1, 0, 1, 3, 4, 1, 1}, 5)
|
|
|
|
// Now shutdown B2
|
|
sb2.Shutdown()
|
|
sa1.WaitForShutdown()
|
|
time.Sleep(250 * time.Millisecond)
|
|
checkInterest(t, []int{0, 1, 0, 0, 2, 0, 1, 1}, 2)
|
|
|
|
ncB1.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Now we will create 2 qsubs to sa2 that is still running.
|
|
ncA2 = natsConnect(t, sa2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
qsub1 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
qsub2 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
// sa1 is down, so 0, sa2 has 2 queue subs, so1 is down, so 0, so2 is up, so
|
|
// 1 for remote interest, same for sb1. Server sb2 is down, so 0, then 1 for
|
|
// remote interest for sc and sd. The expected tally for the gateway to "C"
|
|
// should be 2.
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 0, 1, 1}, 2)
|
|
|
|
// Close the leaf connection between sb1 and sa2
|
|
sa2.mu.Lock()
|
|
for _, l := range sa2.leafs {
|
|
l.mu.Lock()
|
|
l.nc.Close()
|
|
l.mu.Unlock()
|
|
}
|
|
sa2.mu.Unlock()
|
|
time.Sleep(50 * time.Millisecond)
|
|
checkLeafNodeConnected(t, sa2)
|
|
// Should be the same counts
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 0, 1, 1}, 2)
|
|
|
|
// Unsubscribe one of the queue sub.
|
|
qsub2.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
time.Sleep(50 * time.Millisecond)
|
|
// One less for the local interest on sa2 and 1 less for the expected GW count.
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 0, 1, 1}, 1)
|
|
|
|
// Verify that interest works by publishing from server "C".
|
|
ncC := natsConnect(t, sc.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncC.Close()
|
|
natsPub(t, ncC, "foo", []byte("hello"))
|
|
|
|
// Message should be received by qsub1.
|
|
natsNexMsg(t, qsub1, time.Second)
|
|
|
|
// Now stop sa2 and instead start sb2.
|
|
ncA2.Close()
|
|
sa2.Shutdown()
|
|
sa2.WaitForShutdown()
|
|
|
|
// Wait for counts to go down to 0
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Restart sb2 to form route to sb1.
|
|
sb2, _ = RunServerWithConfig(sb2Conf)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
waitForOutboundGateways(t, sb2, 1, time.Second)
|
|
|
|
// Create 2 queue subs on sb2.
|
|
ncB2 = natsConnect(t, sb2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
|
|
qsub1 = natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
qsub2 = natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsFlush(t, ncB2)
|
|
// sa1 and sa2 are down, so 0, so1 is down, so 0, so2 is up, so 1 for remote interest,
|
|
// same for sb1. Server sb2 has 2 local queue subs, so 2. Servers sc and sd have 1 for
|
|
// remote interest. The expected tally for the gateway to "C" should be 2.
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 2, 1, 1}, 2)
|
|
|
|
// Now close the route(s) between sb1 and sb2.
|
|
sb2.mu.Lock()
|
|
sb2.forEachRoute(func(r *client) {
|
|
r.mu.Lock()
|
|
r.nc.Close()
|
|
r.mu.Unlock()
|
|
})
|
|
sb2.mu.Unlock()
|
|
time.Sleep(50 * time.Millisecond)
|
|
checkClusterFormed(t, sb1, sb2)
|
|
// Should be the same counts
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 2, 1, 1}, 2)
|
|
|
|
// Unsubscribe one of the queue sub.
|
|
qsub2.Unsubscribe()
|
|
natsFlush(t, ncB2)
|
|
time.Sleep(50 * time.Millisecond)
|
|
// One less for the local interest on sb2 and 1 less for the expected GW count.
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
// Verify that interest works by publishing from server "C".
|
|
natsPub(t, ncC, "foo", []byte("hello"))
|
|
|
|
// Message should be received by qsub1.
|
|
natsNexMsg(t, qsub1, time.Second)
|
|
}
|
|
|
|
func TestLeafNodeQueueInterestAndWeightCorrectAfterServerRestartOrConnectionClose(t *testing.T) {
|
|
|
|
// Note that this is not what a normal configuration should be. Users should
|
|
// configure each leafnode to have the URLs of both B1 and B2 so that when
|
|
// a server fails, the leaf can reconnect to the other running server. But
|
|
// we force it to be this way to demonstrate what the issue was and see that
|
|
// it is now fixed.
|
|
//
|
|
// B1 <--- route ---> B2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// A1 <--- route ---> A2
|
|
//
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
pinnedAccount string
|
|
}{
|
|
{"without pinned account", _EMPTY_},
|
|
{"with pinned account", "accounts: [\"A\"]"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
leafBConf := `
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_, test.pinnedAccount)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), test.pinnedAccount)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
leafAConf := `
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
remotes: [
|
|
{
|
|
url: "nats://a:[email protected]:%d"
|
|
account: A
|
|
}
|
|
]
|
|
no_advertise: true
|
|
}
|
|
`
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, "A1", _EMPTY_, test.pinnedAccount, b1Opts.LeafNode.Port)))
|
|
a1, a1Opts := RunServerWithConfig(a1Conf)
|
|
defer a1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, "A2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", a1Opts.Cluster.Port), test.pinnedAccount, b2Opts.LeafNode.Port)))
|
|
a2, _ := RunServerWithConfig(a2Conf)
|
|
defer a2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a2)
|
|
checkClusterFormed(t, a1, a2)
|
|
|
|
// Create a client on A2 and 3 queue subs.
|
|
ncA2 := natsConnect(t, a2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncA2.Close()
|
|
|
|
var qsubs []*nats.Subscription
|
|
for i := 0; i < 3; i++ {
|
|
qsubs = append(qsubs, natsQueueSub(t, ncA2, "foo", "queue", func(_ *nats.Msg) {}))
|
|
}
|
|
natsFlush(t, ncA2)
|
|
|
|
subj := "foo"
|
|
checkInterest := func(expected bool) {
|
|
t.Helper()
|
|
for _, s := range []*Server{a1, a2, b1, b2} {
|
|
acc, err := s.LookupAccount("A")
|
|
require_NoError(t, err)
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
i := acc.Interest(subj)
|
|
if expected && i == 0 {
|
|
return fmt.Errorf("Still no interest on %q in server %q", subj, s)
|
|
} else if !expected && i > 0 {
|
|
return fmt.Errorf("Still interest on %q in server %q", subj, s)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
checkInterest(true)
|
|
|
|
// Check that Leafz from A1 (which connects to B1) has the expected sub
|
|
// interest on "foo".
|
|
checkLeafA1 := func(expected bool) {
|
|
t.Helper()
|
|
// We will wait a bit before checking Leafz since with the bug, it would
|
|
// take a bit of time after the action to reproduce the issue for the
|
|
// LS+ to be sent to the wrong cluster, or the interest to not be removed.
|
|
time.Sleep(100 * time.Millisecond)
|
|
// Now check Leafz
|
|
leafsz, err := a1.Leafz(&LeafzOptions{Subscriptions: true})
|
|
require_NoError(t, err)
|
|
require_Equal(t, leafsz.NumLeafs, 1)
|
|
require_True(t, leafsz.Leafs[0] != nil)
|
|
lz := leafsz.Leafs[0]
|
|
require_Equal(t, lz.Name, "B1")
|
|
require_Equal(t, lz.NumSubs, uint32(len(lz.Subs)))
|
|
var ok bool
|
|
for _, sub := range lz.Subs {
|
|
if sub == "foo" {
|
|
if expected {
|
|
ok = true
|
|
break
|
|
}
|
|
t.Fatalf("Did not expect to have the %q subscription", sub)
|
|
}
|
|
}
|
|
if expected && !ok {
|
|
t.Fatalf("Expected to have the %q subscription", "foo")
|
|
}
|
|
}
|
|
checkLeafA1(false)
|
|
|
|
// Now restart server "B1". We need to create a conf file with the ports
|
|
// that it used.
|
|
restartBConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: B1
|
|
listen: "127.0.0.1:%d"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:%d"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:%d"
|
|
no_advertise: true
|
|
}
|
|
`, b1Opts.Port, b1Opts.Cluster.Port, test.pinnedAccount, b1Opts.LeafNode.Port)))
|
|
b1.Shutdown()
|
|
b1, _ = RunServerWithConfig(restartBConf)
|
|
defer b1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
// Stop one of the queue sub.
|
|
qsubs[0].Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
|
|
// Check that "foo" does not show up in the subscription list
|
|
// for the leaf from A1 to B1.
|
|
checkLeafA1(false)
|
|
|
|
// Now stop the other 2 and check again.
|
|
qsubs[1].Unsubscribe()
|
|
qsubs[2].Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
checkInterest(false)
|
|
|
|
checkLeafA1(false)
|
|
|
|
// Now recreate 3 queue subs.
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSub(t, ncA2, "foo", "queue", func(_ *nats.Msg) {})
|
|
}
|
|
// Check interest is present in all servers
|
|
checkInterest(true)
|
|
// But A1's leaf to B1 should still not have a sub interest for "foo".
|
|
checkLeafA1(false)
|
|
|
|
// Now stop the client connection instead of removing queue sub
|
|
// one at a time. This will ensure that we properly handle an LS-
|
|
// on B2 with an interest with a queue weight more than 1 still
|
|
// present at the time of processing.
|
|
ncA2.Close()
|
|
checkInterest(false)
|
|
|
|
checkLeafA1(false)
|
|
|
|
// We will now test that if the queue subs are created on B2,
|
|
// we have proper interest on A1, but when we close the connection,
|
|
// the interest disappears.
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncB2.Close()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSub(t, ncB2, "foo", "queue", func(_ *nats.Msg) {})
|
|
}
|
|
checkInterest(true)
|
|
checkLeafA1(true)
|
|
// Close the connection, so all queue subs should be removed at once.
|
|
ncB2.Close()
|
|
checkInterest(false)
|
|
checkLeafA1(false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeQueueWeightCorrectOnRestart(t *testing.T) {
|
|
leafBConf := `
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port))))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
leafAConf := `
|
|
server_name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [{url: "nats://127.0.0.1:%d"}]
|
|
reconnect: "50ms"
|
|
}
|
|
`
|
|
aConf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, b2Opts.LeafNode.Port)))
|
|
a, _ := RunServerWithConfig(aConf)
|
|
defer a.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
|
|
nc := natsConnect(t, a.ClientURL())
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
natsQueueSubSync(t, nc, "foo", "queue")
|
|
}
|
|
natsFlush(t, nc)
|
|
|
|
checkQueueWeight := func() {
|
|
for _, s := range []*Server{b1, b2} {
|
|
gacc := s.GlobalAccount()
|
|
gacc.mu.RLock()
|
|
sl := gacc.sl
|
|
gacc.mu.RUnlock()
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
// For remote queue interest, Match() will expand to queue weight.
|
|
// So we should have 1 group and 2 queue subs present.
|
|
res := sl.Match("foo")
|
|
for _, qsubs := range res.qsubs {
|
|
for _, sub := range qsubs {
|
|
if string(sub.subject) == "foo" && string(sub.queue) == "queue" && atomic.LoadInt32(&sub.qw) == 2 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("Server %q does not have expected queue interest with expected weight", s)
|
|
})
|
|
}
|
|
}
|
|
checkQueueWeight()
|
|
|
|
// Now restart server "B2". We need to create a conf file with the ports
|
|
// that it used.
|
|
restartBConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: B2
|
|
listen: "127.0.0.1:%d"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:%d"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:%d"
|
|
no_advertise: true
|
|
}
|
|
`, b2Opts.Port, b2Opts.Cluster.Port, fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), b2Opts.LeafNode.Port)))
|
|
b2.Shutdown()
|
|
b2, _ = RunServerWithConfig(restartBConf)
|
|
defer b2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
checkQueueWeight()
|
|
}
|
|
|
|
func TestLeafNodeRoutedSubKeyDifferentBetweenLeafSubAndRoutedSub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
pinnedAccount string
|
|
lnocu bool
|
|
}{
|
|
{"without pinned account", _EMPTY_, true},
|
|
{"with pinned account", "accounts: [\"XYZ\"]", true},
|
|
{"old server without pinned account", _EMPTY_, false},
|
|
{"old server with pinned account", "accounts: [\"XYZ\"]", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
leafBConf := `
|
|
accounts: {XYZ {users:[{user:a, password:pwd}]}}
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_, test.pinnedAccount)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), test.pinnedAccount)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
// To make route connections behave like if the server was connected
|
|
// to an older server, change the routes' lnocu field.
|
|
if !test.lnocu {
|
|
for _, s := range []*Server{b1, b2} {
|
|
s.mu.RLock()
|
|
s.forEachRoute(func(r *client) {
|
|
r.mu.Lock()
|
|
r.route.lnocu = false
|
|
r.mu.Unlock()
|
|
})
|
|
s.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// This leaf will have a cluster name that matches an account name.
|
|
// The idea is to make sure that hub servers are not using incorrect
|
|
// keys to differentiate a routed queue interest on subject "A" with
|
|
// queue name "foo" for account "A" in their cluster: "RS+ A A foo"
|
|
// with a leafnode plain subscription, which since there is an origin
|
|
// would be: "LS+ A A foo", that is, origin is "A", account is "A"
|
|
// and subject is "foo".
|
|
leafAConf := `
|
|
accounts: {XYZ {users:[{user:a, password:pwd}]}}
|
|
server_name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: XYZ
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
remotes: [
|
|
{
|
|
url: "nats://a:[email protected]:%d"
|
|
account: "XYZ"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
aConf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, b2Opts.LeafNode.Port)))
|
|
a, _ := RunServerWithConfig(aConf)
|
|
defer a.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncB2.Close()
|
|
// Create a plain sub on "foo"
|
|
natsSubSync(t, ncB2, "foo")
|
|
// And a queue sub on "XYZ" with queue name "foo"
|
|
natsQueueSubSync(t, ncB2, "XYZ", "foo")
|
|
natsFlush(t, ncB2)
|
|
|
|
ncA := natsConnect(t, a.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncA.Close()
|
|
// From the leafnode, create a plain sub on "foo"
|
|
natsSubSync(t, ncA, "foo")
|
|
// And a queue sub on "XYZ" with queue name "foo"
|
|
natsQueueSubSync(t, ncA, "XYZ", "foo")
|
|
natsFlush(t, ncA)
|
|
|
|
// Check the acc.rm on B2
|
|
b2Acc, err := b2.LookupAccount("XYZ")
|
|
require_NoError(t, err)
|
|
|
|
rsubKey := keyFromSubWithOrigin(&subscription{subject: []byte("foo")})
|
|
rqsubKey := keyFromSubWithOrigin(&subscription{subject: []byte("XYZ"), queue: []byte("foo")})
|
|
rlsubKey := keyFromSubWithOrigin(&subscription{origin: []byte("XYZ"), subject: []byte("foo")})
|
|
rlqsubKey := keyFromSubWithOrigin(&subscription{origin: []byte("XYZ"), subject: []byte("XYZ"), queue: []byte("foo")})
|
|
// Ensure all keys are different
|
|
require_True(t, rsubKey != rqsubKey && rqsubKey != rlsubKey && rlsubKey != rlqsubKey)
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
b2Acc.mu.RLock()
|
|
defer b2Acc.mu.RUnlock()
|
|
for _, key := range []string{rsubKey, rqsubKey, rlsubKey, rlqsubKey} {
|
|
v, ok := b2Acc.rm[key]
|
|
if !ok {
|
|
return fmt.Errorf("Did not find key %q for sub: %+v", key, sub)
|
|
}
|
|
if v != 1 {
|
|
return fmt.Errorf("Key %q v=%v for sub: %+v", key, v, sub)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now check that on B1, we have 2 distinct subs for the route.
|
|
b1Acc, err := b1.LookupAccount("XYZ")
|
|
require_NoError(t, err)
|
|
|
|
var route *client
|
|
|
|
if test.pinnedAccount == _EMPTY_ {
|
|
b1Acc.mu.RLock()
|
|
rIdx := b1Acc.routePoolIdx
|
|
b1Acc.mu.RUnlock()
|
|
b1.mu.RLock()
|
|
b1.forEachRouteIdx(rIdx, func(r *client) bool {
|
|
route = r
|
|
return false
|
|
})
|
|
b1.mu.RUnlock()
|
|
} else {
|
|
b1.mu.RLock()
|
|
remotes := b1.accRoutes["XYZ"]
|
|
for _, r := range remotes {
|
|
route = r
|
|
break
|
|
}
|
|
b1.mu.RUnlock()
|
|
}
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
// Check that route.subs has 4 entries for the subs we
|
|
// created in this test.
|
|
var entries []string
|
|
route.mu.Lock()
|
|
for key := range route.subs {
|
|
if strings.Contains(key, "foo") {
|
|
entries = append(entries, key)
|
|
}
|
|
}
|
|
route.mu.Unlock()
|
|
// With new servers, we expect 4 entries, but with older servers,
|
|
// we have collisions and have only 2.
|
|
var expected int
|
|
if test.lnocu {
|
|
expected = 4
|
|
} else {
|
|
expected = 2
|
|
}
|
|
if len(entries) != expected {
|
|
return fmt.Errorf("Expected %d entries with %q, got this: %q", expected, "foo", entries)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Close the connections and expect all gone.
|
|
ncB2.Close()
|
|
ncA.Close()
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
b2Acc.mu.RLock()
|
|
defer b2Acc.mu.RUnlock()
|
|
for _, key := range []string{rsubKey, rqsubKey, rlsubKey, rlqsubKey} {
|
|
if _, ok := b2Acc.rm[key]; ok {
|
|
return fmt.Errorf("Key %q still present", key)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
var entries []string
|
|
route.mu.Lock()
|
|
for key := range route.subs {
|
|
if strings.Contains(key, "foo") {
|
|
entries = append(entries, key)
|
|
}
|
|
}
|
|
route.mu.Unlock()
|
|
if len(entries) != 0 {
|
|
return fmt.Errorf("Still routed subscriptions on %q: %q", "foo", entries)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
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:[email protected]:%d, account: A},
|
|
{url:nats://s1:[email protected]:%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 time.Time
|
|
connCh chan time.Time
|
|
}
|
|
|
|
func (l *checkLeafMinVersionLogger) Errorf(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "minimum version") {
|
|
select {
|
|
case l.errCh <- time.Now():
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *checkLeafMinVersionLogger) Noticef(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "Leafnode connection created") {
|
|
select {
|
|
case l.connCh <- time.Now():
|
|
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 time.Time, 1), connCh: make(chan time.Time, 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")
|
|
}
|
|
|
|
var rejectAt time.Time
|
|
select {
|
|
case rejectAt = <-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, the next attempt should be
|
|
// delayed by the dedicated minimum version reconnect delay on the soliciting
|
|
// side, not by the normal reconnect interval.
|
|
select {
|
|
case secondAttemptAt := <-l.connCh:
|
|
if elapsed := secondAttemptAt.Sub(rejectAt); elapsed < leafNodeMinVersionReconnectDelay {
|
|
t.Fatalf("Expected reconnect attempt after at least %v, got %v", leafNodeMinVersionReconnectDelay, elapsed)
|
|
}
|
|
case <-time.After(7 * time.Second):
|
|
t.Fatal("Did not get the reconnect attempt")
|
|
}
|
|
}
|
|
|
|
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:[email protected]:%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)
|
|
}
|
|
|
|
dto := DefaultTestOptions
|
|
lopts := &dto
|
|
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 ...any) {
|
|
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:[email protected]" } ]
|
|
}
|
|
trace = true
|
|
`))
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// Connect client to the hub.
|
|
nc, err := nats.Connect(s.ClientURL())
|
|
require_NoError(t, err)
|
|
defer nc.Close()
|
|
|
|
// 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:[email protected]: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:[email protected]: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:[email protected]: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:[email protected]: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 TestLeafNodeTLSHandshakeFirstFallbackDelayConfigValues(t *testing.T) {
|
|
tmpl := `
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
timeout: 1
|
|
first: %s
|
|
}
|
|
}
|
|
`
|
|
for _, test := range []struct {
|
|
name string
|
|
val string
|
|
first bool
|
|
delay time.Duration
|
|
}{
|
|
{"first as boolean true", "true", true, 0},
|
|
{"first as boolean false", "false", false, 0},
|
|
{"first as string true", "\"true\"", true, 0},
|
|
{"first as string false", "\"false\"", false, 0},
|
|
{"first as string on", "on", true, 0},
|
|
{"first as string off", "off", false, 0},
|
|
{"first as string auto", "auto", true, DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY},
|
|
{"first as string auto_fallback", "auto_fallback", true, DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY},
|
|
{"first as fallback duration", "300ms", true, 300 * time.Millisecond},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, test.val)))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
if test.first {
|
|
if !o.LeafNode.TLSHandshakeFirst {
|
|
t.Fatal("Expected tls first to be true, was not")
|
|
}
|
|
if test.delay != o.LeafNode.TLSHandshakeFirstFallback {
|
|
t.Fatalf("Expected fallback delay to be %v, got %v", test.delay, o.LeafNode.TLSHandshakeFirstFallback)
|
|
}
|
|
} else {
|
|
if o.LeafNode.TLSHandshakeFirst {
|
|
t.Fatal("Expected tls first to be false, was not")
|
|
}
|
|
if o.LeafNode.TLSHandshakeFirstFallback != 0 {
|
|
t.Fatalf("Expected fallback delay to be 0, got %v", o.LeafNode.TLSHandshakeFirstFallback)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// Shutdown the remote
|
|
s2.Shutdown()
|
|
|
|
// Reload the hub with handshake_first with a duration
|
|
reloadUpdateConfig(t, s1, confHub, fmt.Sprintf(tmpl1, `"250ms"`))
|
|
|
|
// Now start s2 with a remote that does not use handshake_first (set to false)
|
|
confSpoke = createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, "false")))
|
|
s2, _ = RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
checkLeafNodeConnected(t, s2)
|
|
}
|
|
|
|
func TestLeafNodeTLSHandshakeEvenForRemoteWithNoTLSBlock(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
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(confHub)
|
|
defer s1.Shutdown()
|
|
|
|
tmpl2 := `
|
|
port: -1
|
|
leafnodes : {
|
|
port : -1
|
|
remotes : [
|
|
{
|
|
urls : [tls://127.0.0.1:%d]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
confSpoke := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
|
|
l := &captureDebugLogger{dbgCh: make(chan string, 100)}
|
|
s2.SetLogger(l, true, false)
|
|
|
|
tm := time.NewTimer(2 * time.Second)
|
|
defer tm.Stop()
|
|
for {
|
|
select {
|
|
case l := <-l.dbgCh:
|
|
if strings.Contains(l, "Starting TLS") {
|
|
// OK!
|
|
return
|
|
}
|
|
case <-tm.C:
|
|
t.Fatalf("Did not perform a TLS handshake")
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
port := s.getOpts().Port
|
|
leafNodePort := s.getOpts().LeafNode.Port
|
|
s.Shutdown()
|
|
|
|
o.Port = port
|
|
o.LeafNode.Port = leafNodePort
|
|
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)
|
|
}
|
|
port := s.getOpts().Port
|
|
leafNodePort := s.getOpts().LeafNode.Port
|
|
s.Shutdown()
|
|
|
|
o.Port = port
|
|
o.LeafNode.Port = leafNodePort
|
|
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:[email protected]:%d", account: "A", compression: s2_better }
|
|
{ url: "nats://b:[email protected]:%d", account: "B", compression: s2_best }
|
|
{ url: "nats://c:[email protected]:%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 BenchmarkLeafNodeCompression(b *testing.B) {
|
|
conf1 := createConfFile(b, []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}] }
|
|
D { users: [{user: d, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
port := o1.LeafNode.Port
|
|
conf2 := createConfFile(b, []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}] }
|
|
D { users: [{user: d, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{ url: "nats://a:[email protected]:%d", account: "A", compression: s2_better }
|
|
{ url: "nats://b:[email protected]:%d", account: "B", compression: s2_best }
|
|
{ url: "nats://c:[email protected]:%d", account: "C", compression: s2_fast }
|
|
{ url: "nats://d:[email protected]:%d", account: "D", compression: off }
|
|
]
|
|
}
|
|
`, port, port, port, port)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(b, s1, 4)
|
|
checkLeafNodeConnectedCount(b, s2, 4)
|
|
|
|
l, err := s2.Leafz(nil)
|
|
require_NoError(b, err)
|
|
for _, r := range l.Leafs {
|
|
switch {
|
|
case r.Account == "A" && r.Compression == CompressionS2Better:
|
|
case r.Account == "B" && r.Compression == CompressionS2Best:
|
|
case r.Account == "C" && r.Compression == CompressionS2Fast:
|
|
case r.Account == "D" && r.Compression == CompressionOff:
|
|
default:
|
|
b.Fatalf("Account %q had incorrect compression mode %q on leaf connection", r.Account, r.Compression)
|
|
}
|
|
}
|
|
|
|
msg := make([]byte, 1024)
|
|
for _, p := range []struct {
|
|
algo string
|
|
user string
|
|
}{
|
|
{"Better", "a"},
|
|
{"Best", "b"},
|
|
{"Fast", "c"},
|
|
{"Off", "d"},
|
|
} {
|
|
nc1 := natsConnect(b, s1.ClientURL(), nats.UserInfo(p.user, "pwd"))
|
|
nc2 := natsConnect(b, s2.ClientURL(), nats.UserInfo(p.user, "pwd"))
|
|
|
|
sub, err := nc1.SubscribeSync("foo")
|
|
require_NoError(b, err)
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
b.Run(p.algo, func(b *testing.B) {
|
|
start := time.Now()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
err = nc2.Publish("foo", msg)
|
|
require_NoError(b, err)
|
|
|
|
_, err = sub.NextMsg(time.Second)
|
|
require_NoError(b, err)
|
|
}
|
|
|
|
b.ReportMetric(float64(len(msg)*b.N)/1024/1024, "MB")
|
|
b.ReportMetric(float64(len(msg)*b.N)/1024/1024/float64(time.Since(start).Seconds()), "MB/sec")
|
|
})
|
|
|
|
nc1.Close()
|
|
nc2.Close()
|
|
}
|
|
}
|
|
|
|
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 TestLeafNodeNoCompressionWithWebsocket(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
acceptWSComp bool
|
|
remoteWSComp bool
|
|
expectS2 bool
|
|
}{
|
|
{"both ws compression", true, true, false},
|
|
{"only accept ws compression", true, false, true},
|
|
{"only remote ws compression", false, true, true},
|
|
{"neither ws compression", false, false, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "A"
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
compression: %v
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
compression: "s2_fast"
|
|
}
|
|
`, test.acceptWSComp)))
|
|
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: %v, compression: "s2_fast"}
|
|
]
|
|
}
|
|
`, o1.Websocket.Port, test.remoteWSComp)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
getLeafState := func(s *Server) (string, bool) {
|
|
var cm string
|
|
var wsComp bool
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
for _, l := range s.leafs {
|
|
l.mu.Lock()
|
|
cm = l.leaf.compression
|
|
if l.ws != nil {
|
|
wsComp = l.ws.compress
|
|
}
|
|
l.mu.Unlock()
|
|
return cm, wsComp
|
|
}
|
|
return _EMPTY_, false
|
|
}
|
|
expected := CompressionOff
|
|
if test.expectS2 {
|
|
expected = CompressionS2Fast
|
|
}
|
|
// WS compression is only actually used when both sides opted in.
|
|
wantWSComp := test.acceptWSComp && test.remoteWSComp
|
|
for _, s := range []*Server{s1, s2} {
|
|
cm, wsComp := getLeafState(s)
|
|
if cm != expected {
|
|
t.Fatalf("Expected leaf compression to be %q, got %q", expected, cm)
|
|
}
|
|
if wsComp != wantWSComp {
|
|
t.Fatalf("Expected ws.compress to be %v, got %v", wantWSComp, wsComp)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Verifies that the initial INFO sent by the accept side over a WS leaf
|
|
// connection does not advertise leafnode compression when WS compression is
|
|
// negotiated. An older soliciting peer (without the WS-aware patch) would
|
|
// honor an advertised mode, switch to S2, and then deadlock waiting for a
|
|
// compressed INFO response that we never send.
|
|
func TestLeafNodeWSAcceptInitialInfoSuppressesCompression(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
negotiatePMC bool
|
|
expectedComp string
|
|
}{
|
|
{"ws compression negotiated", true, _EMPTY_},
|
|
{"ws compression not negotiated", false, CompressionS2Fast},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "A"
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
compression: true
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
compression: "s2_fast"
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.Websocket.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
wsKey, err := wsMakeChallengeKey()
|
|
require_NoError(t, err)
|
|
|
|
var reqBuf bytes.Buffer
|
|
fmt.Fprintf(&reqBuf, "GET %s HTTP/1.1\r\n", leafNodeWSPath)
|
|
fmt.Fprintf(&reqBuf, "Host: %s\r\n", addr)
|
|
reqBuf.WriteString("Upgrade: websocket\r\n")
|
|
reqBuf.WriteString("Connection: Upgrade\r\n")
|
|
fmt.Fprintf(&reqBuf, "Sec-WebSocket-Key: %s\r\n", wsKey)
|
|
reqBuf.WriteString("Sec-WebSocket-Version: 13\r\n")
|
|
if test.negotiatePMC {
|
|
fmt.Fprintf(&reqBuf, "Sec-WebSocket-Extensions: %s\r\n", wsPMCReqHeaderValue)
|
|
}
|
|
reqBuf.WriteString("\r\n")
|
|
|
|
require_NoError(t, c.SetDeadline(time.Now().Add(2*time.Second)))
|
|
_, err = c.Write(reqBuf.Bytes())
|
|
require_NoError(t, err)
|
|
|
|
br := bufio.NewReader(c)
|
|
resp, err := http.ReadResponse(br, nil)
|
|
require_NoError(t, err)
|
|
resp.Body.Close()
|
|
require_True(t, resp.StatusCode == http.StatusSwitchingProtocols)
|
|
|
|
pmcAccepted := strings.Contains(resp.Header.Get("Sec-WebSocket-Extensions"), wsPMCExtension)
|
|
if test.negotiatePMC != pmcAccepted {
|
|
t.Fatalf("Expected PMC negotiated=%v, got %v", test.negotiatePMC, pmcAccepted)
|
|
}
|
|
|
|
// Read the first WS frame header (binary, possibly compressed).
|
|
b0, err := br.ReadByte()
|
|
require_NoError(t, err)
|
|
require_True(t, b0&wsFinalBit != 0)
|
|
require_True(t, wsOpCode(b0&0xF) == wsBinaryMessage)
|
|
require_True(t, (b0&wsRsv1Bit != 0) == pmcAccepted)
|
|
|
|
b1, err := br.ReadByte()
|
|
require_NoError(t, err)
|
|
require_True(t, b1&wsMaskBit == 0)
|
|
plen := int(b1 & 0x7F)
|
|
switch plen {
|
|
case 126:
|
|
var b [2]byte
|
|
_, err = io.ReadFull(br, b[:])
|
|
require_NoError(t, err)
|
|
plen = int(b[0])<<8 | int(b[1])
|
|
case 127:
|
|
t.Fatalf("Frame too large")
|
|
}
|
|
payload := make([]byte, plen)
|
|
_, err = io.ReadFull(br, payload)
|
|
require_NoError(t, err)
|
|
|
|
if pmcAccepted {
|
|
// Append the 4-byte trailer stripped by permessage-deflate plus
|
|
// a final empty block so flate.NewReader sees end-of-stream.
|
|
payload = append(payload, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff)
|
|
fr := flate.NewReader(bytes.NewReader(payload))
|
|
payload, err = io.ReadAll(fr)
|
|
require_NoError(t, err)
|
|
fr.Close()
|
|
}
|
|
|
|
require_True(t, bytes.HasPrefix(payload, []byte("INFO ")))
|
|
payload = bytes.TrimSuffix(payload, []byte(_CRLF_))
|
|
var info Info
|
|
require_NoError(t, json.Unmarshal(payload[5:], &info))
|
|
if info.Compression != test.expectedComp {
|
|
t.Fatalf("Expected info.Compression=%q, got %q", test.expectedComp, info.Compression)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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"} }
|
|
]
|
|
exports [ { stream: "RESPONSE" } ]
|
|
}
|
|
STL {
|
|
users = [ { user: "stl", pass: "p" } ]
|
|
exports [ { stream: "REQUEST" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
KSC {
|
|
users = [ { user: "ksc", pass: "p" } ]
|
|
exports [ { stream: "REQUEST" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
|
|
}`
|
|
|
|
sc := createJetStreamSuperClusterWithTemplate(t, tmpl, 5, 2)
|
|
defer sc.shutdown()
|
|
|
|
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, STL and KSC.
|
|
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, RESPONSE], deny_import: RESPONSE }
|
|
]
|
|
}`
|
|
|
|
// 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()
|
|
}
|
|
// Let's them propagate
|
|
time.Sleep(100 * time.Millisecond)
|
|
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()
|
|
|
|
// Drain can lose some messages since it only checks pending messages known to the client,
|
|
// and is not aware of other messages that are inflight on other servers.
|
|
numMin := num * 99 / 100
|
|
checkFor(t, time.Second, 200*time.Millisecond, func() error {
|
|
total := int(r1.Load() + r2.Load())
|
|
if total >= numMin {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all received: %d vs %d (minimum %d)", total, num, numMin)
|
|
})
|
|
require_True(t, r2.Load() > r1.Load())
|
|
|
|
// Now check opposite flow for responses.
|
|
|
|
// Create 10 subscribers.
|
|
var rsubs []*nats.Subscription
|
|
|
|
for i := 0; i < 10; i++ {
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
// Let's them propagate
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
nc, _ = jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
_, err := nc.SubscribeSync("RESPONSE")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
|
|
// Now connect and send responses from EFG in cloud.
|
|
nc, _ = jsClientConnect(t, sc.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
checkAllRespReceived := func() error {
|
|
p := pending(rsubs)
|
|
if p == 100 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all responses received: %d vs %d", p, 100)
|
|
}
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
}
|
|
|
|
func TestLeafNodeWithWeightedDQResponsesWithStreamImportAccountsWithUnsub(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
|
|
exports [ { stream: "RESPONSE" } ]
|
|
}
|
|
STL {
|
|
users = [ { user: "stl", pass: "p" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
KSC {
|
|
users = [ { user: "ksc", pass: "p" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
|
|
}`
|
|
|
|
c := createJetStreamClusterWithTemplate(t, tmpl, "US-CENTRAL", 3)
|
|
defer c.shutdown()
|
|
|
|
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, STL and KSC.
|
|
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 ] } ]
|
|
}`
|
|
|
|
genLeafTmpl := func(tmpl string) string {
|
|
t.Helper()
|
|
|
|
var ln []string
|
|
for _, s := range c.servers {
|
|
lno := s.getOpts().LeafNode
|
|
ln = append(ln, fmt.Sprintf("nats://ksc:p@%s:%d", lno.Host, lno.Port))
|
|
}
|
|
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln, ", ")), 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, 1)
|
|
}
|
|
|
|
// Create 10 subscribers.
|
|
var rsubs []*nats.Subscription
|
|
|
|
closeSubs := func(subs []*nats.Subscription) {
|
|
for _, sub := range subs {
|
|
sub.Unsubscribe()
|
|
}
|
|
}
|
|
|
|
checkAllRespReceived := func() error {
|
|
t.Helper()
|
|
var total int
|
|
for _, sub := range rsubs {
|
|
n, _, err := sub.Pending()
|
|
require_NoError(t, err)
|
|
total += n
|
|
}
|
|
if total == 100 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all responses received: %d vs %d", total, 100)
|
|
}
|
|
|
|
s := ln.randomServer()
|
|
for i := 0; i < 4; i++ {
|
|
nc, _ := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
|
|
// Now connect and send responses from EFG in cloud.
|
|
nc, _ := jsClientConnect(t, c.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc.Close()
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
// Make sure all received.
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
|
|
checkAccountInterest := func(s *Server, accName string) *SublistResult {
|
|
t.Helper()
|
|
acc, err := s.LookupAccount(accName)
|
|
require_NoError(t, err)
|
|
acc.mu.RLock()
|
|
r := acc.sl.Match("RESPONSE")
|
|
acc.mu.RUnlock()
|
|
return r
|
|
}
|
|
|
|
checkInterest := func() error {
|
|
t.Helper()
|
|
for _, s := range c.servers {
|
|
if r := checkAccountInterest(s, "KSC"); len(r.psubs)+len(r.qsubs) > 0 {
|
|
return fmt.Errorf("Subs still present for %q: %+v", "KSC", r)
|
|
}
|
|
if r := checkAccountInterest(s, "EFG"); len(r.psubs)+len(r.qsubs) > 0 {
|
|
return fmt.Errorf("Subs still present for %q: %+v", "EFG", r)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Now unsub them and create new ones on a different server.
|
|
closeSubs(rsubs)
|
|
rsubs = rsubs[:0]
|
|
|
|
// Also restart the server that we had all the rsubs on.
|
|
s.Shutdown()
|
|
s.WaitForShutdown()
|
|
s = ln.restartServer(s)
|
|
ln.waitOnClusterReady()
|
|
ln.waitOnServerCurrent(s)
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkInterest)
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nc, _ := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
// Make sure all received.
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
|
|
closeSubs(rsubs)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkInterest)
|
|
}
|
|
|
|
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:[email protected]:%d"
|
|
local: "A"
|
|
}
|
|
{
|
|
url: "nats://ha:[email protected]:%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:[email protected]:%d"
|
|
local: "A"
|
|
}
|
|
{
|
|
url: "nats://HA:[email protected]:%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)
|
|
}
|
|
createConn(sh1, "HA")
|
|
createConn(sh2, "HA")
|
|
createConn(sp1, "A")
|
|
createConn(sp2, "A")
|
|
createConn(sp1, "B")
|
|
createConn(sp2, "B")
|
|
for _, nc := range conns {
|
|
defer nc.Close()
|
|
}
|
|
|
|
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:[email protected]:%[1]d
|
|
local: "A"
|
|
}
|
|
{
|
|
url: nats://hub1:[email protected]:%[1]d
|
|
local: "C"
|
|
}
|
|
{
|
|
url: nats://hub2:[email protected]:%[2]d
|
|
local: "A"
|
|
}
|
|
{
|
|
url: nats://hub2:[email protected]:%[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)
|
|
}
|
|
|
|
func TestLeafNodeSlowConsumer(t *testing.T) {
|
|
ao := DefaultOptions()
|
|
ao.LeafNode.Host = "127.0.0.1"
|
|
ao.LeafNode.Port = -1
|
|
ao.WriteDeadline = 1 * time.Millisecond
|
|
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)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
a.mu.Lock()
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
a.grMu.Lock()
|
|
defer a.grMu.Unlock()
|
|
for _, cli := range a.grTmpClients {
|
|
cli.out.wdl = time.Nanosecond
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
a.mu.Unlock()
|
|
|
|
// Only leafnode slow consumers that made it past connect are tracked
|
|
// in the slow consumers counter.
|
|
if _, err := c.Write([]byte("CONNECT {}\r\n")); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
// Read info
|
|
br := bufio.NewReader(c)
|
|
br.ReadLine()
|
|
for i := 0; i < 10; i++ {
|
|
if _, err := c.Write([]byte("PING\r\n")); err != nil {
|
|
t.Fatalf("Unexpected error writing PING: %v", err)
|
|
}
|
|
}
|
|
defer c.Close()
|
|
timeout := time.Now().Add(time.Second)
|
|
var (
|
|
got uint64
|
|
expected uint64 = 1
|
|
)
|
|
for time.Now().Before(timeout) {
|
|
got = a.NumSlowConsumersLeafs()
|
|
if got == expected {
|
|
return
|
|
}
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
t.Fatalf("Timed out waiting for slow consumer leafnodes, got: %v, expected: %v", got, expected)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4367
|
|
func TestLeafNodeDQMultiAccountExportImport(t *testing.T) {
|
|
bConf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: cluster-b-0
|
|
accounts {
|
|
$SYS: { users: [ { user: admin, password: pwd } ] },
|
|
AGG: {
|
|
exports: [ { service: "PING.>" } ]
|
|
users: [ { user: agg, password: agg } ]
|
|
}
|
|
}
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`))
|
|
|
|
sb, ob := RunServerWithConfig(bConf)
|
|
defer sb.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: { store_dir: '%s' }
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
accounts {
|
|
$SYS: { users: [ { user: admin, password: pwd } ] },
|
|
A: {
|
|
mappings: { "A.>" : ">" }
|
|
exports: [ { service: A.> } ]
|
|
users: [ { user: a, password: a } ]
|
|
},
|
|
AGG: {
|
|
imports: [ { service: { subject: A.>, account: A } } ]
|
|
users: [ { user: agg, password: agg } ]
|
|
},
|
|
}
|
|
leaf {
|
|
remotes: [ {
|
|
urls: [ nats-leaf://agg:[email protected]:{LEAF_PORT} ]
|
|
account: AGG
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", ob.LeafNode.Port), 1)
|
|
c := createJetStreamCluster(t, tmpl, "cluster-a", "cluster-a-", 3, 22110, false)
|
|
defer c.shutdown()
|
|
|
|
// Make sure all servers are connected via leafnode to the hub, the b server.
|
|
for _, s := range c.servers {
|
|
checkLeafNodeConnectedCount(t, s, 1)
|
|
}
|
|
|
|
// Connect to a server in the cluster and create a DQ listener.
|
|
nc, _ := jsClientConnect(t, c.randomServer(), nats.UserInfo("a", "a"))
|
|
defer nc.Close()
|
|
|
|
var got atomic.Int32
|
|
|
|
natsQueueSub(t, nc, "PING", "Q", func(m *nats.Msg) {
|
|
got.Add(1)
|
|
m.Respond([]byte("REPLY"))
|
|
})
|
|
|
|
// Now connect to B and send the request.
|
|
ncb, _ := jsClientConnect(t, sb, nats.UserInfo("agg", "agg"))
|
|
defer ncb.Close()
|
|
|
|
_, err := ncb.Request("A.PING", []byte("REQUEST"), time.Second)
|
|
require_NoError(t, err)
|
|
require_Equal(t, got.Load(), 1)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4934
|
|
func TestLeafNodeServerReloadSubjectMappings(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
mappings = { "source1": "target" }
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ]
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Create our subscriber.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
sub := natsSubSync(t, nc, "target")
|
|
natsFlush(t, nc)
|
|
|
|
// Create our publisher.
|
|
ncl := natsConnect(t, l.ClientURL())
|
|
defer ncl.Close()
|
|
// Publish our message.
|
|
ncl.Publish("source1", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub, 1)
|
|
|
|
// Now change mapping.
|
|
reloadUpdateConfig(t, s, conf, strings.Replace(stmpl, "source1", "source2", 1))
|
|
// Also make sure we do not have subscription interest for source1 on leaf anymore.
|
|
checkSubInterest(t, l, globalAccountName, "source2", 2*time.Second)
|
|
|
|
// Publish our new message.
|
|
ncl.Publish("source2", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub, 2)
|
|
|
|
// Also make sure we do not have subscription interest for source1 on leaf anymore.
|
|
checkSubNoInterest(t, l, globalAccountName, "source1", 2*time.Second)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5099
|
|
func TestLeafNodeServerReloadSubjectMappingsWithSameSubject(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
mappings = { "source": "target1" }
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ { urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ] } ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Create our subscriber.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
sub1 := natsSubSync(t, nc, "target1")
|
|
sub2 := natsSubSync(t, nc, "target2")
|
|
natsFlush(t, nc)
|
|
|
|
// Create our publisher.
|
|
ncl := natsConnect(t, l.ClientURL())
|
|
defer ncl.Close()
|
|
// Publish our message.
|
|
ncl.Publish("source", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub1, 1)
|
|
// Make sure the other does not.
|
|
checkSubsPending(t, sub2, 0)
|
|
|
|
// Now change mapping, but only the "to" subject, keeping same "from"
|
|
reloadUpdateConfig(t, s, conf, strings.Replace(stmpl, "target1", "target2", 1))
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Publish our new message.
|
|
ncl.Publish("source", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub2, 1)
|
|
// Make sure the other does not.
|
|
checkSubsPending(t, sub1, 1)
|
|
}
|
|
|
|
func TestLeafNodeNkeyAuth(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
authorization: { nkey: UCSTG5CRF5GEJERAFKUUYRODGABTBVWY2NPE4GGKRQVQOH74PIAKTVKO }
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
url: nats-leaf://127.0.0.1:{LEAF_PORT}
|
|
seed: SUACJN3OSKWWPQXME4JUNFJ3PARXPO657GGNWNU7PK7G3AUQQYHLW26XH4
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
}
|
|
|
|
func TestLeafNodeAccountNkeysAuth(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
accounts {
|
|
A { users [ {nkey: UCSTG5CRF5GEJERAFKUUYRODGABTBVWY2NPE4GGKRQVQOH74PIAKTVKO } ] }
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
url: nats-leaf://127.0.0.1:{LEAF_PORT}
|
|
seed: SUACJN3OSKWWPQXME4JUNFJ3PARXPO657GGNWNU7PK7G3AUQQYHLW26XH4
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5117
|
|
func TestLeafNodeLoopDetectionOnActualLoop(t *testing.T) {
|
|
// Setup: B --[leaf]--> A C --[leaf]--> A C --[leaf] --> B
|
|
accConf := `
|
|
accounts: {
|
|
APP: {
|
|
users: [ { user:u, password: u,
|
|
permissions: { publish = "u.>", subscribe = "u.>" }} ]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}`
|
|
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a1
|
|
port: -1
|
|
cluster: { name: A }
|
|
leafnodes {
|
|
port: 17422
|
|
}
|
|
%s`, accConf)))
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: b1
|
|
port: -1
|
|
cluster: { name: B }
|
|
leafnodes {
|
|
port: 17432
|
|
remotes [
|
|
{ urls: ["nats-leaf://u:u@localhost:17422"], account: "APP" }
|
|
]
|
|
reconnect: "2s"
|
|
}
|
|
%s`, accConf)))
|
|
|
|
confC := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: c1
|
|
port: -1
|
|
cluster: { name: C }
|
|
leafnodes {
|
|
port: 17442
|
|
remotes [
|
|
{ urls: ["nats-leaf://u:u@localhost:17422"], account: "APP" }
|
|
# This one creates the loop
|
|
{ urls: ["nats-leaf://u:u@localhost:17432"], account: "APP" }
|
|
]
|
|
reconnect: "0.5s"
|
|
}
|
|
%s`, accConf)))
|
|
|
|
// Start order will be B -> C -> A
|
|
// We will force C to connect to A first before B using different reconnect intervals.
|
|
// If B connects first we detect loops fine. If C connects first we do not.
|
|
|
|
srvB, _ := RunServerWithConfig(confB)
|
|
defer srvB.Shutdown()
|
|
lb := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvB.SetLogger(lb, false, false)
|
|
|
|
srvC, _ := RunServerWithConfig(confC)
|
|
defer srvC.Shutdown()
|
|
lc := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvC.SetLogger(lc, false, false)
|
|
|
|
// C should connect to B
|
|
checkLeafNodeConnectedCount(t, srvC, 1)
|
|
|
|
srvA, _ := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
la := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvA.SetLogger(la, false, false)
|
|
|
|
select {
|
|
case <-la.ch:
|
|
case <-lb.ch:
|
|
case <-lc.ch:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("Did not get any error regarding loop")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeConnectionSucceedsEvenWithDelayedFirstINFO(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
websocket bool
|
|
}{
|
|
{"regular", false},
|
|
{"websocket", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ob := DefaultOptions()
|
|
ob.ServerName = "HUB"
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
ob.LeafNode.AuthTimeout = 10
|
|
if test.websocket {
|
|
ob.Websocket.Host = "127.0.0.1"
|
|
ob.Websocket.Port = -1
|
|
ob.Websocket.HandshakeTimeout = 10 * time.Second
|
|
ob.Websocket.AuthTimeout = 10
|
|
ob.Websocket.NoTLS = true
|
|
}
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
var port int
|
|
var scheme string
|
|
if test.websocket {
|
|
port = ob.Websocket.Port
|
|
scheme = wsSchemePrefix
|
|
} else {
|
|
port = ob.LeafNode.Port
|
|
scheme = "nats"
|
|
}
|
|
|
|
urlStr := fmt.Sprintf("%s://127.0.0.1:%d", scheme, port)
|
|
proxy := createNetProxy(1100*time.Millisecond, 1024*1024*1024, 1024*1024*1024, urlStr, true)
|
|
defer proxy.stop()
|
|
proxyURL := proxy.clientURL()
|
|
_, proxyPort, err := net.SplitHostPort(proxyURL[len(scheme)+3:])
|
|
require_NoError(t, err)
|
|
|
|
lnBURL, err := url.Parse(fmt.Sprintf("%s://127.0.0.1:%s", scheme, proxyPort))
|
|
require_NoError(t, err)
|
|
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "SPOKE"
|
|
oa.Cluster.Name = "xyz"
|
|
remote := &RemoteLeafOpts{
|
|
URLs: []*url.URL{lnBURL},
|
|
FirstInfoTimeout: 3 * time.Second,
|
|
}
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
})
|
|
}
|
|
}
|
|
|
|
type captureLeafConnClosed struct {
|
|
DummyLogger
|
|
ch chan struct{}
|
|
}
|
|
|
|
func (l *captureLeafConnClosed) Noticef(format string, v ...any) {
|
|
msg := fmt.Sprintf(format, v...)
|
|
if strings.Contains(msg, "Leafnode connection closed: Read Error") {
|
|
select {
|
|
case l.ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeDetectsStaleConnectionIfNoInfo(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
websocket bool
|
|
}{
|
|
{"regular", false},
|
|
{"websocket", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require_NoError(t, err)
|
|
defer l.Close()
|
|
|
|
ch := make(chan struct{})
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
<-ch
|
|
}()
|
|
|
|
var scheme string
|
|
if test.websocket {
|
|
scheme = wsSchemePrefix
|
|
} else {
|
|
scheme = "nats"
|
|
}
|
|
urlStr := fmt.Sprintf("%s://%s", scheme, l.Addr())
|
|
lnBURL, err := url.Parse(urlStr)
|
|
require_NoError(t, err)
|
|
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "SPOKE"
|
|
oa.Cluster.Name = "xyz"
|
|
remote := &RemoteLeafOpts{
|
|
URLs: []*url.URL{lnBURL},
|
|
FirstInfoTimeout: 250 * time.Millisecond,
|
|
}
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
oa.DisableShortFirstPing = false
|
|
oa.NoLog = false
|
|
sa, err := NewServer(oa)
|
|
require_NoError(t, err)
|
|
defer sa.Shutdown()
|
|
|
|
log := &captureLeafConnClosed{ch: make(chan struct{}, 1)}
|
|
sa.SetLogger(log, false, false)
|
|
sa.Start()
|
|
|
|
select {
|
|
case <-log.ch:
|
|
// OK
|
|
case <-time.After(750 * time.Millisecond):
|
|
t.Fatalf("Connection was not closed")
|
|
}
|
|
|
|
sa.Shutdown()
|
|
close(ch)
|
|
wg.Wait()
|
|
sa.WaitForShutdown()
|
|
})
|
|
}
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5473
|
|
func TestLeafNodeDupeDeliveryQueueSubAndPlainSub(t *testing.T) {
|
|
clusterCommonConf := `
|
|
accounts: {
|
|
tenant: {
|
|
users: [ { user:t, password: t } ]
|
|
exports: [{stream: system-a.events.>}]
|
|
}
|
|
system-a: {
|
|
users: [ { user:sa, password: sa } ]
|
|
imports: [
|
|
{stream: {subject: system-a.events.>, account: tenant}, prefix: tenant}
|
|
]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}
|
|
leafnodes {
|
|
remotes: [{
|
|
urls: [ "nats-leaf://sa:[email protected]:17422" ]
|
|
account: system-a
|
|
}]
|
|
}`
|
|
|
|
confCluster0 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a-0
|
|
port: -1
|
|
cluster: {
|
|
name: cluster-a
|
|
listen: 127.0.0.1:16122
|
|
routes = [ nats://127.0.0.1:16123 ]
|
|
pool_size: -1
|
|
}
|
|
%s`, clusterCommonConf)))
|
|
|
|
confCluster1 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a-1
|
|
port: -1
|
|
cluster: {
|
|
name: cluster-a
|
|
listen: 127.0.0.1:16123
|
|
routes = [ nats://127.0.0.1:16122 ]
|
|
pool_size: -1
|
|
}
|
|
%s`, clusterCommonConf)))
|
|
|
|
serverB := createConfFile(t, []byte(`
|
|
server_name: b
|
|
port: -1
|
|
leafnodes: { port: 17422 }
|
|
accounts: {
|
|
system-a: {
|
|
users: [ { user: sa, password: sa } ]
|
|
exports: [{stream: *.system-a.>}]
|
|
}
|
|
system-b: {
|
|
users: [ { user: sb, password: sb } ]
|
|
imports: [ {stream: {subject: *.system-a.>, account: system-a }}]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}`))
|
|
|
|
// Start server B
|
|
srvB, _ := RunServerWithConfig(serverB)
|
|
defer srvB.Shutdown()
|
|
|
|
// Start the cluster servers.
|
|
srvA0, _ := RunServerWithConfig(confCluster0)
|
|
defer srvA0.Shutdown()
|
|
// Make sure this is connected first before starting the second server in cluster A.
|
|
checkLeafNodeConnectedCount(t, srvB, 1)
|
|
// Start second A server.
|
|
srvA1, _ := RunServerWithConfig(confCluster1)
|
|
defer srvA1.Shutdown()
|
|
// Make sure they are routed together.
|
|
checkNumRoutes(t, srvA0, 1)
|
|
checkNumRoutes(t, srvA1, 1)
|
|
// Make sure each cluster server is connected to server B.
|
|
checkLeafNodeConnectedCount(t, srvB, 2)
|
|
|
|
// Create plain subscriber on server B attached to system-b account.
|
|
ncB := natsConnect(t, srvB.ClientURL(), nats.UserInfo("sb", "sb"))
|
|
defer ncB.Close()
|
|
sub := natsSubSync(t, ncB, "*.system-a.events.>")
|
|
subq := natsQueueSubSync(t, ncB, "*.system-a.events.objectnotfound", "SBQ")
|
|
natsFlush(t, ncB)
|
|
|
|
// Create a subscription on SA1 (we will send from SA0). We want to make sure that
|
|
// when subscription on B is removed, this does not affect the subject interest
|
|
// in SA0 on behalf of SA1.
|
|
ncSAA1 := natsConnect(t, srvA1.ClientURL(), nats.UserInfo("sa", "sa"))
|
|
defer ncSAA1.Close()
|
|
sub2 := natsSubSync(t, ncSAA1, "*.system-a.events.>")
|
|
subq2 := natsQueueSubSync(t, ncSAA1, "*.system-a.events.objectnotfound", "SBQ")
|
|
natsFlush(t, ncSAA1)
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Connect to cluster A on SA0.
|
|
ncA := natsConnect(t, srvA0.ClientURL(), nats.UserInfo("t", "t"))
|
|
defer ncA.Close()
|
|
|
|
natsPub(t, ncA, "system-a.events.objectnotfound", []byte("EventA"))
|
|
natsFlush(t, ncA)
|
|
|
|
natsNexMsg(t, sub, time.Second)
|
|
natsNexMsg(t, sub2, time.Second)
|
|
if _, err := subq.NextMsg(250 * time.Millisecond); err != nil {
|
|
natsNexMsg(t, subq2, time.Second)
|
|
}
|
|
|
|
// Unsubscribe the subscriptions from server B.
|
|
natsUnsub(t, sub)
|
|
natsUnsub(t, subq)
|
|
natsFlush(t, ncB)
|
|
|
|
// Wait for subject propagation.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Publish again, subscriptions on SA1 should receive it.
|
|
natsPub(t, ncA, "system-a.events.objectnotfound", []byte("EventA"))
|
|
natsFlush(t, ncA)
|
|
|
|
natsNexMsg(t, sub2, time.Second)
|
|
natsNexMsg(t, subq2, time.Second)
|
|
}
|
|
|
|
func TestLeafNodeServerKickClient(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf { remotes: [ { urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ] } ] }
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// We want to make sure we can kick the leafnode connections as well as client connections.
|
|
conns, err := s.Connz(&ConnzOptions{Account: globalAccountName})
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(conns.Conns), 1)
|
|
lid := conns.Conns[0].Cid
|
|
|
|
disconnectTime := time.Now()
|
|
err = s.DisconnectClientByID(lid)
|
|
require_NoError(t, err)
|
|
|
|
// Wait until we are reconnected.
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
// Look back up again and make sure start time indicates a restart, meaning kick worked.
|
|
conns, err = s.Connz(&ConnzOptions{Account: globalAccountName})
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(conns.Conns), 1)
|
|
ln := conns.Conns[0]
|
|
require_True(t, lid != ln.Cid)
|
|
require_True(t, ln.Start.After(disconnectTime))
|
|
}
|
|
|
|
func TestLeafNodeBannerNoClusterNameIfNoCluster(t *testing.T) {
|
|
u, err := url.Parse("nats://127.0.0.1:1234")
|
|
require_NoError(t, err)
|
|
|
|
opts := DefaultOptions()
|
|
opts.ServerName = "LEAF_SERVER"
|
|
opts.Cluster.Name = _EMPTY_
|
|
opts.Cluster.Port = 0
|
|
opts.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
opts.NoLog = false
|
|
|
|
s, err := NewServer(opts)
|
|
require_NoError(t, err)
|
|
defer func() {
|
|
s.Shutdown()
|
|
s.WaitForShutdown()
|
|
}()
|
|
l := &captureNoticeLogger{}
|
|
s.SetLogger(l, false, false)
|
|
s.Start()
|
|
|
|
if !s.ReadyForConnections(time.Second) {
|
|
t.Fatal("Server not ready!")
|
|
}
|
|
|
|
l.Lock()
|
|
for _, n := range l.notices {
|
|
if strings.Contains(n, "Cluster: ") {
|
|
l.Unlock()
|
|
t.Fatalf("Cluster name should not be displayed, got %q", n)
|
|
}
|
|
}
|
|
l.Unlock()
|
|
}
|
|
|
|
func TestLeafNodeCredFormatting(t *testing.T) {
|
|
//create the operator/sys/account tree
|
|
oKP, err := nkeys.CreateOperator()
|
|
require_NoError(t, err)
|
|
oPK, err := oKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
oc := jwt.NewOperatorClaims(oPK)
|
|
oc.Name = "O"
|
|
oJWT, err := oc.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
sysKP, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
sysPK, err := sysKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
sys := jwt.NewAccountClaims(sysPK)
|
|
sys.Name = "SYS"
|
|
sysJWT, err := sys.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
aKP, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
aPK, err := aKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
ac := jwt.NewAccountClaims(aPK)
|
|
ac.Name = "A"
|
|
aJWT, err := ac.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
uKP, err := nkeys.CreateUser()
|
|
require_NoError(t, err)
|
|
uSeed, err := uKP.Seed()
|
|
require_NoError(t, err)
|
|
uPK, err := uKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
// build the config
|
|
stmpl := fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: MEM
|
|
resolver_preload: {
|
|
%s: %s
|
|
%s: %s
|
|
}
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`, oJWT, sysPK, sysPK, sysJWT, aPK, aJWT)
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// create the leaf node
|
|
// generate the user credentials
|
|
uc := jwt.NewUserClaims(uPK)
|
|
uc.Name = "U"
|
|
uc.Limits.Data = -1
|
|
uc.Limits.Payload = -1
|
|
uc.Permissions.Pub.Allow.Add(">")
|
|
uc.Permissions.Sub.Allow.Add(">")
|
|
uJWT, err := uc.Encode(aKP)
|
|
require_NoError(t, err)
|
|
|
|
runLeaf := func(t *testing.T, creds []byte) {
|
|
file, err := os.CreateTemp("", "tmp-*.creds")
|
|
require_NoError(t, err)
|
|
_, err = file.Write(creds)
|
|
require_NoError(t, err)
|
|
require_NoError(t, file.Close())
|
|
|
|
template := fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
leafnodes {
|
|
remotes: [
|
|
{
|
|
urls: [ nats-leaf://127.0.0.1:%d ]
|
|
credentials: "%s"
|
|
}
|
|
]
|
|
}`, o.LeafNode.Port, file.Name())
|
|
conf := createConfFile(t, []byte(template))
|
|
leaf, _ := RunServerWithConfig(conf)
|
|
defer leaf.Shutdown()
|
|
defer os.Remove(file.Name())
|
|
checkLeafNodeConnected(t, leaf)
|
|
}
|
|
|
|
creds, err := jwt.FormatUserConfig(uJWT, uSeed)
|
|
require_NoError(t, err)
|
|
|
|
runLeaf(t, creds)
|
|
runLeaf(t, bytes.ReplaceAll(creds, []byte{'\n'}, []byte{'\r', '\n'}))
|
|
}
|
|
|
|
func TestLeafNodePermissionWithLiteralSubjectAndQueueInterest(t *testing.T) {
|
|
hconf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts {
|
|
A {
|
|
users: [
|
|
{ user: "user", password: "pwd",
|
|
permissions: {
|
|
subscribe: { allow: ["_INBOX.>", "my.subject"] }
|
|
publish: {allow: [">"]}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hconf)
|
|
defer hub.Shutdown()
|
|
|
|
lconf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [
|
|
{url: "nats://user:[email protected]:%d", account: A}
|
|
]
|
|
}
|
|
accounts {
|
|
A { users: [{user: user, password: pwd}] }
|
|
}
|
|
`, ohub.LeafNode.Port)))
|
|
leaf, _ := RunServerWithConfig(lconf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncLeaf.Close()
|
|
natsQueueSub(t, ncLeaf, "my.subject", "queue", func(m *nats.Msg) {
|
|
m.Respond([]byte("OK"))
|
|
})
|
|
natsFlush(t, ncLeaf)
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncHub.Close()
|
|
|
|
var resp *nats.Msg
|
|
var err error
|
|
checkFor(t, 5*time.Second, time.Second, func() error {
|
|
// Make sure we don't fail the test on the first "no responders", might
|
|
// take time for the sub to propagate.
|
|
resp, err = ncHub.Request("my.subject", []byte("hello"), time.Second)
|
|
return err
|
|
})
|
|
require_Equal(t, "OK", string(resp.Data))
|
|
}
|
|
|
|
func TestLeafNodePermissionWithGateways(t *testing.T) {
|
|
usConf := createConfFile(t, []byte(`
|
|
server_name: "US"
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: "US"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts {
|
|
sys { users: [{user: sys, password: sys}] }
|
|
leaf { users: [{user: leaf, password: leaf}] }
|
|
}
|
|
system_account: sys
|
|
`))
|
|
us, ous := RunServerWithConfig(usConf)
|
|
defer us.Shutdown()
|
|
|
|
euConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "EU"
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: "EU"
|
|
listen: "127.0.0.1:-1"
|
|
gateways: [
|
|
{
|
|
name: "US"
|
|
urls: ["nats://127.0.0.1:%d"]
|
|
}
|
|
]
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts {
|
|
sys { users: [{user: sys, password: sys}] }
|
|
leaf {
|
|
users: [
|
|
{
|
|
user: leaf
|
|
password: leaf
|
|
permissions: {
|
|
publish: "bar"
|
|
subscribe: "foo"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
system_account: sys
|
|
`, ous.Gateway.Port))
|
|
eu, oeu := RunServerWithConfig(euConf)
|
|
defer eu.Shutdown()
|
|
|
|
waitForOutboundGateways(t, us, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, eu, 1, 2*time.Second)
|
|
waitForInboundGateways(t, us, 1, 2*time.Second)
|
|
waitForInboundGateways(t, eu, 1, 2*time.Second)
|
|
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [
|
|
{ url: "nats://leaf:[email protected]:%d" }
|
|
]
|
|
}
|
|
`, oeu.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
// Run the service from EU leafnode.
|
|
ncEU := natsConnect(t, leaf.ClientURL())
|
|
defer ncEU.Close()
|
|
natsSub(t, ncEU, "foo", func(m *nats.Msg) {
|
|
m.Respond([]byte("response"))
|
|
})
|
|
natsFlush(t, ncEU)
|
|
|
|
// Wait for subject interest to propagate.
|
|
checkGWInterestOnlyModeInterestOn(t, us, "EU", "leaf", "foo")
|
|
|
|
// Connect to the US server
|
|
ncUS := natsConnect(t, us.ClientURL(), nats.UserInfo("leaf", "leaf"))
|
|
defer ncUS.Close()
|
|
|
|
// Create a subscription on "bar" and send request on "foo"
|
|
sub := natsSubSync(t, ncUS, "bar")
|
|
// Wait for subject to propagate so we know that EU server
|
|
// would know about the "bar" subscription.
|
|
checkGWInterestOnlyModeInterestOn(t, eu, "US", "leaf", "bar")
|
|
// Send the request and make sure we receive the reply.
|
|
natsPubReq(t, ncUS, "foo", "bar", []byte("request"))
|
|
reply := natsNexMsg(t, sub, time.Second)
|
|
if string(reply.Data) != "response" {
|
|
t.Fatalf("Invalid response: %q", reply.Data)
|
|
}
|
|
// Make sure that we don't blindly accept any reply because there
|
|
// is the routing protocol. So create a sub on "baz" that the leaf
|
|
// would not be allowed to reply to. We should not get the reply
|
|
// to our request.
|
|
sub2 := natsSubSync(t, ncUS, "baz")
|
|
checkGWInterestOnlyModeInterestOn(t, eu, "US", "leaf", "baz")
|
|
// Send the request. We should not get the message since the
|
|
// leaf server does not have permission to publish on "baz".
|
|
natsPubReq(t, ncUS, "foo", "baz", []byte("request2"))
|
|
if msg, err := sub2.NextMsg(250 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have received the reply, got %q", msg.Data)
|
|
}
|
|
}
|
|
|
|
// Test that the inbound LMSG path checks for subject permissions.
|
|
// Normally, a leaf node will check for ACLs before sending LMSG messages.
|
|
// If it doesn't we still need to perform ACL checks on the hub side.
|
|
func TestLeafNodeLMSGHonorsPublishPermissions(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: ["pub.ok"] }
|
|
subscribe: { allow: [">"] }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
hlog := &captureErrorLogger{errCh: make(chan string, 4)}
|
|
hub.SetLogger(hlog, false, false)
|
|
|
|
leafConn, err := net.DialTimeout("tcp", net.JoinHostPort(ohub.LeafNode.Host, fmt.Sprintf("%d", ohub.LeafNode.Port)), 2*time.Second)
|
|
require_NoError(t, err)
|
|
defer leafConn.Close()
|
|
|
|
br := bufio.NewReader(leafConn)
|
|
_, err = br.ReadString('\n')
|
|
require_NoError(t, err)
|
|
|
|
connectOp := fmt.Appendf(nil, "CONNECT {\"name\":\"leaf\",\"user\":%q,\"pass\":%q}\r\n", "leaf", "pwd")
|
|
_, err = leafConn.Write(connectOp)
|
|
require_NoError(t, err)
|
|
_, err = leafConn.Write([]byte("PING\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
|
|
subOK := natsSubSync(t, ncHub, "pub.ok")
|
|
subBlocked := natsSubSync(t, ncHub, "pub.blocked")
|
|
natsFlush(t, ncHub)
|
|
|
|
_, err = fmt.Fprintf(leafConn, "LMSG %s %d\r\n%s\r\n", "pub.ok", len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
msg := natsNexMsg(t, subOK, time.Second)
|
|
require_Equal(t, "ok", string(msg.Data))
|
|
|
|
_, err = fmt.Fprintf(leafConn, "LMSG %s %d\r\n%s\r\n", "pub.blocked", len("blocked"), "blocked")
|
|
require_NoError(t, err)
|
|
if msg, err := subBlocked.NextMsg(250 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have received the blocked publish, got %q", msg.Data)
|
|
}
|
|
|
|
select {
|
|
case errMsg := <-hlog.errCh:
|
|
if !strings.Contains(errMsg, `Publish Violation on "pub.blocked"`) {
|
|
t.Fatalf("Expected publish violation log, got %q", errMsg)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get expected publish violation log")
|
|
}
|
|
}
|
|
|
|
// Test that the hub side inbound leaf check still honors tracked reply
|
|
// permissions such as allow_responses after the hub has tracked a reply
|
|
// subject for a request sent to the leaf.
|
|
func TestLeafNodeHubSideLMSGHonorsAllowResponses(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: ["svc.req"] }
|
|
subscribe: { allow: ["svc.req"] }
|
|
allow_responses: true
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
}
|
|
]
|
|
}
|
|
`, ohub.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncLeaf.Close()
|
|
reqCh := make(chan string, 1)
|
|
natsSub(t, ncLeaf, "svc.req", func(m *nats.Msg) {
|
|
select {
|
|
case reqCh <- m.Reply:
|
|
default:
|
|
}
|
|
})
|
|
natsFlush(t, ncLeaf)
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
|
|
reply := ncHub.NewRespInbox()
|
|
sub := natsSubSync(t, ncHub, reply)
|
|
natsFlush(t, ncHub)
|
|
|
|
natsPubReq(t, ncHub, "svc.req", reply, []byte("request"))
|
|
|
|
var trackedReply string
|
|
select {
|
|
case trackedReply = <-reqCh:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not receive request on leaf side")
|
|
}
|
|
require_Equal(t, reply, trackedReply)
|
|
|
|
var leafConn *client
|
|
leaf.mu.Lock()
|
|
for _, leafConn = range leaf.leafs {
|
|
break
|
|
}
|
|
leaf.mu.Unlock()
|
|
if leafConn == nil {
|
|
t.Fatal("Expected a leaf connection on leaf side")
|
|
}
|
|
|
|
_, err := fmt.Fprintf(leafConn.nc, "LMSG %s %d\r\n%s\r\n", reply, len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
|
|
resp := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, "ok", string(resp.Data))
|
|
}
|
|
|
|
// Test that allow_responses works when the tracked reply subject has a
|
|
// gateway routing prefix ($GNR...). deliverMsg tracks the reply under
|
|
// the GW-routed form, so processInboundLeafMsg must also check
|
|
// responseAllowed with the original (non-stripped) subject.
|
|
func TestLeafNodeHubSideLMSGHonorsAllowResponsesGatewayRouted(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: ["svc.req"] }
|
|
subscribe: { allow: ["svc.req"] }
|
|
allow_responses: true
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConn, err := net.DialTimeout("tcp",
|
|
net.JoinHostPort(ohub.LeafNode.Host, fmt.Sprintf("%d", ohub.LeafNode.Port)),
|
|
2*time.Second)
|
|
require_NoError(t, err)
|
|
defer leafConn.Close()
|
|
|
|
br := bufio.NewReader(leafConn)
|
|
_, err = br.ReadString('\n')
|
|
require_NoError(t, err)
|
|
|
|
connectOp := fmt.Appendf(nil,
|
|
"CONNECT {\"name\":\"leaf\",\"user\":%q,\"pass\":%q}\r\n", "leaf", "pwd")
|
|
_, err = leafConn.Write(connectOp)
|
|
require_NoError(t, err)
|
|
_, err = leafConn.Write([]byte("PING\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
|
|
// Simulate the tracking that deliverMsg would perform when
|
|
// delivering a gateway-routed request to the leaf: it stores
|
|
// the reply under the full GW-routed form ($GNR...).
|
|
gwRoutedReply := gwReplyPrefix + "AAAAAAA1.BBBBBBB2._INBOX.test"
|
|
|
|
var hubLeaf *client
|
|
hub.mu.Lock()
|
|
for _, hubLeaf = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
require_NotNil(t, hubLeaf)
|
|
|
|
hubLeaf.mu.Lock()
|
|
hubLeaf.replies[gwRoutedReply] = &resp{t: time.Now()}
|
|
hubLeaf.mu.Unlock()
|
|
|
|
// Subscribe on the hub so we can verify delivery.
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
sub := natsSubSync(t, ncHub, gwRoutedReply)
|
|
natsFlush(t, ncHub)
|
|
|
|
// The leaf sends LMSG with the GW-routed reply as subject.
|
|
// processInboundLeafMsg strips the $GNR prefix before the
|
|
// permission check but must still find the tracked reply.
|
|
_, err = fmt.Fprintf(leafConn, "LMSG %s %d\r\n%s\r\n",
|
|
gwRoutedReply, len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
|
|
resp := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, "ok", string(resp.Data))
|
|
}
|
|
|
|
// A leaf user with publish: { allow: [] } and allow_responses should
|
|
// deny arbitrary publishes but still permit tracked reply responses.
|
|
func TestLeafNodeHubSideLMSGEmptyAllowWithAllowResponses(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: [] }
|
|
subscribe: { allow: [">"] }
|
|
allow_responses: true
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConn, err := net.DialTimeout("tcp",
|
|
net.JoinHostPort(ohub.LeafNode.Host, fmt.Sprintf("%d", ohub.LeafNode.Port)),
|
|
2*time.Second)
|
|
require_NoError(t, err)
|
|
defer leafConn.Close()
|
|
|
|
br := bufio.NewReader(leafConn)
|
|
_, err = br.ReadString('\n')
|
|
require_NoError(t, err)
|
|
|
|
connectOp := fmt.Appendf(nil,
|
|
"CONNECT {\"name\":\"leaf\",\"user\":%q,\"pass\":%q}\r\n", "leaf", "pwd")
|
|
_, err = leafConn.Write(connectOp)
|
|
require_NoError(t, err)
|
|
_, err = leafConn.Write([]byte("PING\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
|
|
// Simulate a tracked reply: inject a reply entry on the
|
|
// hub's leaf connection, then verify the leaf can respond on it.
|
|
reply := "_INBOX.test123"
|
|
|
|
var hubLeaf *client
|
|
hub.mu.Lock()
|
|
for _, hubLeaf = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
require_NotNil(t, hubLeaf)
|
|
|
|
hubLeaf.mu.Lock()
|
|
hubLeaf.replies[reply] = &resp{t: time.Now()}
|
|
hubLeaf.mu.Unlock()
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
|
|
subReply := natsSubSync(t, ncHub, reply)
|
|
natsFlush(t, ncHub)
|
|
|
|
_, err = fmt.Fprintf(leafConn, "LMSG %s %d\r\n%s\r\n",
|
|
reply, len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
|
|
msg := natsNexMsg(t, subReply, time.Second)
|
|
require_Equal(t, "ok", string(msg.Data))
|
|
|
|
// Arbitrary publish should be denied (and closes the connection).
|
|
subBlocked := natsSubSync(t, ncHub, "not.allowed")
|
|
natsFlush(t, ncHub)
|
|
|
|
_, err = fmt.Fprintf(leafConn, "LMSG %s %d\r\n%s\r\n",
|
|
"not.allowed", len("blocked"), "blocked")
|
|
require_NoError(t, err)
|
|
if msg, err := subBlocked.NextMsg(250 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have received arbitrary publish, got %q", msg.Data)
|
|
}
|
|
}
|
|
|
|
// Test that the hub side allows JetStream API traffic.
|
|
// This guards the regression where the hub rejected $JS.API.*
|
|
func TestLeafNodeHubSideLMSGAllowsJSAPI(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
jetstream: { store_dir: %q }
|
|
accounts {
|
|
A {
|
|
jetstream: enabled
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{ user: "leaf", password: "pwd" }
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`, t.TempDir())))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
default_js_domain: {A:""}
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
}
|
|
]
|
|
}
|
|
`, ohub.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
var l *client
|
|
hub.mu.Lock()
|
|
for _, l = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
if l == nil {
|
|
t.Fatal("Expected a leaf connection")
|
|
}
|
|
|
|
l.mu.Lock()
|
|
r := l.perms.sub.deny.Match(jsAllAPI)
|
|
hasSyntheticJSDeny := len(r.psubs)+len(r.qsubs) > 0
|
|
l.mu.Unlock()
|
|
if !hasSyntheticJSDeny {
|
|
t.Fatal("Expected synthetic JetStream deny on accepted leaf connection")
|
|
}
|
|
|
|
checkSubInterest(t, leaf, "A", JSApiAccountInfo, time.Second)
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncLeaf.Close()
|
|
|
|
msg, err := ncLeaf.Request(JSApiAccountInfo, nil, time.Second)
|
|
require_NoError(t, err)
|
|
if !bytes.Contains(msg.Data, []byte(JSApiAccountInfoResponseType)) {
|
|
t.Fatalf("Expected JetStream account info response, got %q", msg.Data)
|
|
}
|
|
}
|
|
|
|
// When the hub has a subject mapping "pub.ok" -> "internal.pub.ok",
|
|
// a leaf allowed to publish "pub.ok" must not be rejected just because
|
|
// "internal.pub.ok" is not in the allow list. The permission check
|
|
// should evaluate the subject the leaf actually sent, not where the
|
|
// hub remaps it locally.
|
|
func TestLeafNodeLMSGPermissionsUseWireSubjectNotMapped(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: ["pub.ok"] }
|
|
subscribe: { allow: [">"] }
|
|
}
|
|
}
|
|
]
|
|
mappings = { "pub.ok": "internal.pub.ok" }
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
}
|
|
]
|
|
}
|
|
`, ohub.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
|
|
// Subscribe to the mapped destination.
|
|
sub := natsSubSync(t, ncHub, "internal.pub.ok")
|
|
natsFlush(t, ncHub)
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncLeaf.Close()
|
|
|
|
// The leaf publishes on the wire subject "pub.ok" which is in
|
|
// the export allow list. The hub maps it to "internal.pub.ok"
|
|
// for local delivery. The ACL check must use the wire subject.
|
|
natsPub(t, ncLeaf, "pub.ok", []byte("hello"))
|
|
natsFlush(t, ncLeaf)
|
|
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, "internal.pub.ok", msg.Subject)
|
|
require_Equal(t, "hello", string(msg.Data))
|
|
}
|
|
|
|
// Test that a solicited leaf configured with hub: true still uses the
|
|
// hub-side export ACLs on inbound LMSG.
|
|
func TestLeafNodeSolicitedHubLMSGHonorsExportPermissions(t *testing.T) {
|
|
spokeConf := createConfFile(t, []byte(`
|
|
server_name: "SPOKE"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{ user: "leaf", password: "pwd" }
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
spoke, ospoke := RunServerWithConfig(spokeConf)
|
|
defer spoke.Shutdown()
|
|
|
|
hubConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
hub: true
|
|
deny_exports: ["pub.blocked"]
|
|
}
|
|
]
|
|
}
|
|
`, ospoke.LeafNode.Port))
|
|
hub, _ := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
hlog := &captureErrorLogger{errCh: make(chan string, 4)}
|
|
hub.SetLogger(hlog, false, false)
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, spoke)
|
|
|
|
var hubLeaf *client
|
|
hub.mu.Lock()
|
|
for _, hubLeaf = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
if hubLeaf == nil {
|
|
t.Fatal("Expected a leaf connection on hub")
|
|
}
|
|
hubLeaf.mu.Lock()
|
|
require_True(t, hubLeaf.isSolicitedLeafNode())
|
|
require_True(t, hubLeaf.isHubLeafNode())
|
|
hubLeaf.mu.Unlock()
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncHub.Close()
|
|
|
|
sub := natsSubSync(t, ncHub, "pub.blocked")
|
|
natsFlush(t, ncHub)
|
|
|
|
var spokeLeaf *client
|
|
spoke.mu.Lock()
|
|
for _, spokeLeaf = range spoke.leafs {
|
|
break
|
|
}
|
|
spoke.mu.Unlock()
|
|
if spokeLeaf == nil {
|
|
t.Fatal("Expected a leaf connection on spoke")
|
|
}
|
|
|
|
_, err := fmt.Fprintf(spokeLeaf.nc, "LMSG %s %d\r\n%s\r\n", "pub.blocked", len("blocked"), "blocked")
|
|
require_NoError(t, err)
|
|
|
|
if msg, err := sub.NextMsg(250 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have received the blocked publish, got %q", msg.Data)
|
|
}
|
|
|
|
select {
|
|
case errMsg := <-hlog.errCh:
|
|
if !strings.Contains(errMsg, `Publish Violation on "pub.blocked"`) {
|
|
t.Fatalf("Expected publish violation log, got %q", errMsg)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get expected publish violation log")
|
|
}
|
|
}
|
|
|
|
// Test that when a solicited remote is configured with hub: true, direct LMSG
|
|
// sent from that hub still respects the local leaf permissions on the accepting
|
|
// spoke server.
|
|
func TestLeafNodeSpokeSideLMSGHonorsLocalPermissions(t *testing.T) {
|
|
spokeConf := createConfFile(t, []byte(`
|
|
server_name: "SPOKE"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{
|
|
user: "leaf"
|
|
password: "pwd"
|
|
permissions: {
|
|
publish: { allow: ["pub.ok"] }
|
|
subscribe: { allow: [">"] }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
spoke, ospoke := RunServerWithConfig(spokeConf)
|
|
defer spoke.Shutdown()
|
|
|
|
hubConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
hub: true
|
|
}
|
|
]
|
|
}
|
|
`, ospoke.LeafNode.Port))
|
|
hub, _ := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, spoke)
|
|
|
|
var hubLeaf *client
|
|
hub.mu.Lock()
|
|
for _, hubLeaf = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
if hubLeaf == nil {
|
|
t.Fatal("Expected a leaf connection on hub")
|
|
}
|
|
hubLeaf.mu.Lock()
|
|
require_True(t, hubLeaf.isSolicitedLeafNode())
|
|
require_True(t, hubLeaf.isHubLeafNode())
|
|
hubLeaf.mu.Unlock()
|
|
|
|
var spokeLeaf *client
|
|
spoke.mu.Lock()
|
|
for _, spokeLeaf = range spoke.leafs {
|
|
break
|
|
}
|
|
spoke.mu.Unlock()
|
|
if spokeLeaf == nil {
|
|
t.Fatal("Expected a leaf connection on spoke")
|
|
}
|
|
spokeLeaf.mu.Lock()
|
|
require_False(t, spokeLeaf.isSolicitedLeafNode())
|
|
require_True(t, spokeLeaf.isSpokeLeafNode())
|
|
spokeLeaf.mu.Unlock()
|
|
|
|
ncSpoke := natsConnect(t, spoke.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncSpoke.Close()
|
|
|
|
sub := natsSubSync(t, ncSpoke, "pub.*")
|
|
natsFlush(t, ncSpoke)
|
|
|
|
_, err := fmt.Fprintf(hubLeaf.nc, "LMSG %s %d\r\n%s\r\n", "pub.blocked", len("blocked"), "blocked")
|
|
require_NoError(t, err)
|
|
_, err = fmt.Fprintf(hubLeaf.nc, "LMSG %s %d\r\n%s\r\n", "pub.ok", len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, "pub.ok", msg.Subject)
|
|
require_Equal(t, "ok", string(msg.Data))
|
|
}
|
|
|
|
// Test that the inbound LMSG path checks for import permissions.
|
|
// Normally, the sender side will check ACLs before sending LMSG messages.
|
|
// If it doesn't we still need to perform ACL checks on the receiving leaf side.
|
|
func TestLeafNodeLMSGHonorsImportPermissions(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [
|
|
{ user: "a", password: "a" }
|
|
{ user: "leaf", password: "pwd" }
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
compression: off
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", password: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
compression: off
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: A
|
|
compression: off
|
|
deny_imports: ["imp.blocked"]
|
|
}
|
|
]
|
|
}
|
|
`, ohub.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncLeaf.Close()
|
|
|
|
sub := natsSubSync(t, ncLeaf, "imp.*")
|
|
natsFlush(t, ncLeaf)
|
|
|
|
leafConn := func() net.Conn {
|
|
var l *client
|
|
hub.mu.Lock()
|
|
for _, l = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
return l.nc
|
|
}
|
|
|
|
ncHubLeaf := leafConn()
|
|
|
|
_, err := fmt.Fprintf(ncHubLeaf, "LMSG %s %d\r\n%s\r\n", "imp.blocked", len("blocked"), "blocked")
|
|
require_NoError(t, err)
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
_, err = fmt.Fprintf(ncHubLeaf, "LMSG %s %d\r\n%s\r\n", "imp.ok", len("ok"), "ok")
|
|
require_NoError(t, err)
|
|
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, "imp.ok", msg.Subject)
|
|
require_Equal(t, "ok", string(msg.Data))
|
|
}
|
|
|
|
func TestLeafNodesDisableRemote(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
leaf1 { users: [{user: leaf1, password: pwd}] }
|
|
leaf2 { users: [{user: leaf2, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
port := ohub.LeafNode.Port
|
|
leafTmpl := `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
reconnect_interval: "50ms"
|
|
remotes: [
|
|
{
|
|
url: "nats://leaf1:[email protected]:%d"
|
|
disabled: %v
|
|
}
|
|
{
|
|
url: "nats://leaf2:[email protected]:%d"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
// Start with "disabled: true" to make sure that we don't solicit it
|
|
// when starting the server.
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, leafTmpl, port, true, port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
// Wait for more than the reconnect interval to make sure that we don't
|
|
// reconnect the connection that should have been disabled.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify that we have only 1 leaf
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
reconnectCh := make(chan struct{}, 2)
|
|
ncl1 := natsConnect(t, hub.ClientURL(), nats.UserInfo("leaf1", "pwd"),
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.ReconnectJitter(time.Millisecond, time.Millisecond),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
reconnectCh <- struct{}{}
|
|
}))
|
|
defer ncl1.Close()
|
|
sub1 := natsSubSync(t, ncl1, "foo")
|
|
natsFlush(t, ncl1)
|
|
|
|
ncl2 := natsConnect(t, hub.ClientURL(), nats.UserInfo("leaf2", "pwd"),
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.ReconnectJitter(time.Millisecond, time.Millisecond),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
reconnectCh <- struct{}{}
|
|
}))
|
|
defer ncl2.Close()
|
|
sub2 := natsSubSync(t, ncl2, "foo")
|
|
natsFlush(t, ncl2)
|
|
|
|
checkSubInterest(t, leaf, globalAccountName, "foo", time.Second)
|
|
|
|
nc := natsConnect(t, leaf.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo", []byte("hello"))
|
|
natsFlush(t, nc)
|
|
|
|
// We should not receive on leaf1
|
|
_, err := sub1.NextMsg(100 * time.Millisecond)
|
|
require_Error(t, err, nats.ErrTimeout)
|
|
|
|
// But should receive on leaf2.
|
|
msg := natsNexMsg(t, sub2, time.Second)
|
|
require_Equal(t, string(msg.Data), "hello")
|
|
|
|
// Enable leaf1, which means set "disabled" to false.
|
|
reloadUpdateConfig(t, leaf, leafConf, fmt.Sprintf(leafTmpl, port, false, port))
|
|
|
|
// Check that we have 2 leaf node connections now.
|
|
checkLeafNodeConnectedCount(t, hub, 2)
|
|
checkLeafNodeConnectedCount(t, leaf, 2)
|
|
|
|
// Verify connectivity.
|
|
natsPub(t, nc, "foo", []byte("hello2"))
|
|
|
|
msg = natsNexMsg(t, sub1, time.Second)
|
|
require_Equal(t, string(msg.Data), "hello2")
|
|
|
|
msg = natsNexMsg(t, sub2, time.Second)
|
|
require_Equal(t, string(msg.Data), "hello2")
|
|
|
|
// Disable again.
|
|
reloadUpdateConfig(t, leaf, leafConf, fmt.Sprintf(leafTmpl, port, true, port))
|
|
|
|
// Wait for more than the reconnect interval to make sure that we don't
|
|
// reconnect the connection that should have been disabled.
|
|
time.Sleep(100 * time.Millisecond)
|
|
// Verify that we have only 1 leaf
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
// Now send a message again.
|
|
natsPub(t, nc, "foo", []byte("hello3"))
|
|
natsFlush(t, nc)
|
|
// We should not receive on leaf1
|
|
_, err = sub1.NextMsg(100 * time.Millisecond)
|
|
require_Error(t, err, nats.ErrTimeout)
|
|
|
|
// We should still receive for leaf2
|
|
msg = natsNexMsg(t, sub2, time.Second)
|
|
require_Equal(t, string(msg.Data), "hello3")
|
|
|
|
// Enable again.
|
|
reloadUpdateConfig(t, leaf, leafConf, fmt.Sprintf(leafTmpl, port, false, port))
|
|
|
|
checkLeafNodeConnectedCount(t, hub, 2)
|
|
checkLeafNodeConnectedCount(t, leaf, 2)
|
|
|
|
// Now shutdown hub.
|
|
hub.Shutdown()
|
|
|
|
// Wait to be disconnected
|
|
checkLeafNodeConnectedCount(t, leaf, 0)
|
|
|
|
// Wait at least some reconnect interval.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Now disable leaf1 one more time to make sure we stop soliciting when we were
|
|
// not currently connected.
|
|
reloadUpdateConfig(t, leaf, leafConf, fmt.Sprintf(leafTmpl, port, true, port))
|
|
|
|
// Restart hub
|
|
hub = RunServer(ohub)
|
|
defer hub.Shutdown()
|
|
|
|
// Verify that we have only 1 leaf
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
// Wait for clients to reconnect
|
|
for range 2 {
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Client failed to reconnect")
|
|
}
|
|
}
|
|
|
|
// Wait for subject propagation
|
|
checkSubInterest(t, leaf, globalAccountName, "foo", time.Second)
|
|
|
|
// Now send a message again.
|
|
natsPub(t, nc, "foo", []byte("hello4"))
|
|
natsFlush(t, nc)
|
|
// We should not receive on leaf1
|
|
_, err = sub1.NextMsg(100 * time.Millisecond)
|
|
require_Error(t, err, nats.ErrTimeout)
|
|
|
|
// We should still receive for leaf2
|
|
msg = natsNexMsg(t, sub2, time.Second)
|
|
require_Equal(t, string(msg.Data), "hello4")
|
|
}
|
|
|
|
func TestLeafNodeIsolatedLeafSubjectPropagationGlobal(t *testing.T) {
|
|
for tname, isolated := range map[string]bool{
|
|
"Isolated": true,
|
|
"Normal": false,
|
|
} {
|
|
t.Run(tname, func(t *testing.T) {
|
|
hubTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
HA { users: [{user: HA, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
isolate_leafnode_interest: %v
|
|
}
|
|
`
|
|
confH := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H1", isolated)))
|
|
sh, oh := RunServerWithConfig(confH)
|
|
defer sh.Shutdown()
|
|
|
|
spokeTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://HA:[email protected]:%d"
|
|
local: "A"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
confSP1 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP1", oh.LeafNode.Port)))
|
|
sp1, _ := RunServerWithConfig(confSP1)
|
|
defer sp1.Shutdown()
|
|
|
|
confSP2 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP2", oh.LeafNode.Port)))
|
|
sp2, _ := RunServerWithConfig(confSP2)
|
|
defer sp2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sh, 2)
|
|
checkLeafNodeConnectedCount(t, sp1, 1)
|
|
checkLeafNodeConnectedCount(t, sp2, 1)
|
|
|
|
// We expect that the hub side will have answered the request from the spoke for
|
|
// isolation if needed, but the spokes themselves will not themselves isolate
|
|
// subscription interest in the other direction unless also configured to do so.
|
|
for _, c := range sh.leafs {
|
|
require_Equal(t, c.leaf.isolated, isolated)
|
|
}
|
|
for _, c := range sp1.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
for _, c := range sp2.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
|
|
nch := natsConnect(t, sh.ClientURL(), nats.UserInfo("HA", "pwd"))
|
|
nc1 := natsConnect(t, sp1.ClientURL(), nats.UserInfo("A", "pwd"))
|
|
|
|
// Create a north-south subscription on the hub that should be visible to both spokes.
|
|
nssub, err := nch.SubscribeSync("northsouth")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "northsouth", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
checkSubInterest(t, sp2, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
|
|
// The spoke subscriptions should be visible to the hub in all cases, but only
|
|
// visible to other spokes if they are not isolated.
|
|
ewsub, err := nc1.SubscribeSync("eastwest")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "eastwest", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "eastwest", time.Second) // Visible to the spoke with the sub.
|
|
if isolated {
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second) // Not visible to the other spoke.
|
|
} else {
|
|
checkSubInterest(t, sp2, "A", "eastwest", time.Second) // Visible to the other spoke.
|
|
}
|
|
require_NoError(t, ewsub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second)
|
|
|
|
// ... but a subscription from the hub should be visible to both.
|
|
require_NoError(t, nssub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "northsouth", time.Second)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeIsolatedLeafSubjectPropagationRequestIsolation(t *testing.T) {
|
|
for tname, isolated := range map[string]bool{
|
|
"Isolated": true,
|
|
"Normal": false,
|
|
} {
|
|
t.Run(tname, func(t *testing.T) {
|
|
hubTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
HA { users: [{user: HA, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`
|
|
confH := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H1")))
|
|
sh, oh := RunServerWithConfig(confH)
|
|
defer sh.Shutdown()
|
|
|
|
spokeTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://HA:[email protected]:%d"
|
|
local: "A"
|
|
request_isolation: %v
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
confSP1 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP1", oh.LeafNode.Port, isolated)))
|
|
sp1, _ := RunServerWithConfig(confSP1)
|
|
defer sp1.Shutdown()
|
|
|
|
confSP2 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP2", oh.LeafNode.Port, isolated)))
|
|
sp2, _ := RunServerWithConfig(confSP2)
|
|
defer sp2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sh, 2)
|
|
checkLeafNodeConnectedCount(t, sp1, 1)
|
|
checkLeafNodeConnectedCount(t, sp2, 1)
|
|
|
|
// We expect that the hub side will have answered the request from the spoke for
|
|
// isolation if needed, but the spokes themselves will not themselves isolate
|
|
// subscription interest in the other direction unless also configured to do so.
|
|
for _, c := range sh.leafs {
|
|
require_Equal(t, c.leaf.isolated, isolated)
|
|
}
|
|
for _, c := range sp1.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
for _, c := range sp2.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
|
|
nch := natsConnect(t, sh.ClientURL(), nats.UserInfo("HA", "pwd"))
|
|
nc1 := natsConnect(t, sp1.ClientURL(), nats.UserInfo("A", "pwd"))
|
|
|
|
// Create a north-south subscription on the hub that should be visible to both spokes.
|
|
nssub, err := nch.SubscribeSync("northsouth")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "northsouth", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
checkSubInterest(t, sp2, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
|
|
// The spoke subscriptions should be visible to the hub in all cases, but only
|
|
// visible to other spokes if they are not isolated.
|
|
ewsub, err := nc1.SubscribeSync("eastwest")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "eastwest", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "eastwest", time.Second) // Visible to the spoke with the sub.
|
|
if isolated {
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second) // Not visible to the other spoke.
|
|
} else {
|
|
checkSubInterest(t, sp2, "A", "eastwest", time.Second) // Visible to the other spoke.
|
|
}
|
|
require_NoError(t, ewsub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second)
|
|
|
|
// ... but a subscription from the hub should be visible to both.
|
|
require_NoError(t, nssub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "northsouth", time.Second)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeIsolatedLeafSubjectPropagationLocalIsolation(t *testing.T) {
|
|
for tname, isolated := range map[string]bool{
|
|
"Isolated": true,
|
|
"Normal": false,
|
|
} {
|
|
t.Run(tname, func(t *testing.T) {
|
|
spokeTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`
|
|
|
|
confSP1 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP1")))
|
|
sp1, osp1 := RunServerWithConfig(confSP1)
|
|
defer sp1.Shutdown()
|
|
|
|
confSP2 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP2")))
|
|
sp2, osp2 := RunServerWithConfig(confSP2)
|
|
defer sp2.Shutdown()
|
|
|
|
hubTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
HA { users: [{user: HA, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{
|
|
url: "nats://A:[email protected]:%d"
|
|
local: "HA"
|
|
isolate: %v
|
|
hub: true
|
|
},
|
|
{
|
|
url: "nats://A:[email protected]:%d"
|
|
local: "HA"
|
|
isolate: %v
|
|
hub: true
|
|
}
|
|
]
|
|
}
|
|
`
|
|
confH := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H1", osp1.LeafNode.Port, isolated, osp2.LeafNode.Port, isolated)))
|
|
sh, _ := RunServerWithConfig(confH)
|
|
defer sh.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sh, 2)
|
|
checkLeafNodeConnectedCount(t, sp1, 1)
|
|
checkLeafNodeConnectedCount(t, sp2, 1)
|
|
|
|
// We expect that the hub side will have answered the request from the spoke for
|
|
// isolation if needed, but the spokes themselves will not themselves isolate
|
|
// subscription interest in the other direction unless also configured to do so.
|
|
for _, c := range sh.leafs {
|
|
require_Equal(t, c.leaf.isolated, isolated)
|
|
}
|
|
for _, c := range sp1.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
for _, c := range sp2.leafs {
|
|
require_False(t, c.leaf.isolated)
|
|
}
|
|
|
|
nch := natsConnect(t, sh.ClientURL(), nats.UserInfo("HA", "pwd"))
|
|
nc1 := natsConnect(t, sp1.ClientURL(), nats.UserInfo("A", "pwd"))
|
|
|
|
// Create a north-south subscription on the hub that should be visible to both spokes.
|
|
nssub, err := nch.SubscribeSync("northsouth")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "northsouth", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
checkSubInterest(t, sp2, "A", "northsouth", time.Second) // Visible to both spokes.
|
|
|
|
// The spoke subscriptions should be visible to the hub in all cases, but only
|
|
// visible to other spokes if they are not isolated.
|
|
ewsub, err := nc1.SubscribeSync("eastwest")
|
|
require_NoError(t, err)
|
|
checkSubInterest(t, sh, "HA", "eastwest", time.Second) // Visible to the hub.
|
|
checkSubInterest(t, sp1, "A", "eastwest", time.Second) // Visible to the spoke with the sub.
|
|
if isolated {
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second) // Not visible to the other spoke.
|
|
} else {
|
|
checkSubInterest(t, sp2, "A", "eastwest", time.Second) // Visible to the other spoke.
|
|
}
|
|
require_NoError(t, ewsub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "eastwest", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "eastwest", time.Second)
|
|
|
|
// ... but a subscription from the hub should be visible to both.
|
|
require_NoError(t, nssub.Unsubscribe())
|
|
checkSubNoInterest(t, sh, "HA", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp1, "A", "northsouth", time.Second)
|
|
checkSubNoInterest(t, sp2, "A", "northsouth", time.Second)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeDaisyChainWithAccountImportExport(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: hub
|
|
listen: "127.0.0.1:-1"
|
|
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts {
|
|
SYS: {
|
|
users: [{ user: s, password: s}],
|
|
},
|
|
ODC: {
|
|
jetstream: enabled
|
|
users: [
|
|
{
|
|
user: u, password: u,
|
|
permissions: {
|
|
publish: {deny: ["local.>","hub2leaf.>"]}
|
|
subscribe: {deny: ["local.>","leaf2leaf.>"]}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
storeDir := t.TempDir()
|
|
leafJSConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: leaf-js
|
|
listen: "127.0.0.1:-1"
|
|
|
|
jetstream {
|
|
store_dir="%s/leaf-js"
|
|
domain=leaf-js
|
|
}
|
|
accounts {
|
|
ODC: {
|
|
jetstream: enabled
|
|
users: [{ user: u, password: u}]
|
|
},
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
urls: ["leaf://u:[email protected]:%d"] # connects to hub
|
|
account: ODC
|
|
}
|
|
]
|
|
}
|
|
`, storeDir, ohub.LeafNode.Port))
|
|
leafJS, _ := RunServerWithConfig(leafJSConf)
|
|
defer leafJS.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leafJS)
|
|
|
|
otherConf := createConfFile(t, []byte(`
|
|
server_name: other
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`))
|
|
other, oother := RunServerWithConfig(otherConf)
|
|
defer other.Shutdown()
|
|
|
|
tmpl := `
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
remotes [
|
|
{
|
|
urls: ["leaf://u:[email protected]:%d"]
|
|
account: ODC_DEV
|
|
}
|
|
{
|
|
urls: ["leaf://127.0.0.1:%d"]
|
|
account: ODC_DEV
|
|
}
|
|
]
|
|
}
|
|
cluster {
|
|
name: "hubsh"
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
accounts: {
|
|
ODC_DEV: {
|
|
users: [
|
|
{user: o, password: o}
|
|
]
|
|
imports: [
|
|
{service: {account: "SH1", subject: "$JS.leaf-sh.API.>"}}
|
|
{stream: {account: "SH1", subject: "sync.leaf-sh.jspush.>"}}
|
|
]
|
|
exports: [
|
|
{stream: ">"}
|
|
{service: ">", response_type: "Singleton"}
|
|
]
|
|
}
|
|
SH1: {
|
|
users: [
|
|
{user: s, password: s}
|
|
]
|
|
exports: [
|
|
{service: "$JS.leaf-sh.API.>", response_type: "Stream"}
|
|
{stream: "sync.leaf-sh.jspush.>"}
|
|
]
|
|
}
|
|
}
|
|
`
|
|
hubSh1Conf := createConfFile(t, fmt.Appendf(nil, tmpl, "hubsh1", ohub.LeafNode.Port, oother.LeafNode.Port, _EMPTY_))
|
|
hubSh1, ohubSh1 := RunServerWithConfig(hubSh1Conf)
|
|
defer hubSh1.Shutdown()
|
|
|
|
hubSh2Conf := createConfFile(t, fmt.Appendf(nil, tmpl, "hubsh2", ohub.LeafNode.Port, oother.LeafNode.Port,
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", ohubSh1.Cluster.Port)))
|
|
hubSh2, ohubSh2 := RunServerWithConfig(hubSh2Conf)
|
|
defer hubSh2.Shutdown()
|
|
|
|
checkClusterFormed(t, hubSh1, hubSh2)
|
|
|
|
checkLeafNodeConnectedCount(t, hub, 3)
|
|
checkLeafNodeConnectedCount(t, hubSh1, 2)
|
|
checkLeafNodeConnectedCount(t, hubSh2, 2)
|
|
|
|
leafShConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: leafsh
|
|
listen: "127.0.0.1:-1"
|
|
|
|
jetstream {
|
|
store_dir="%s/leafsh"
|
|
domain=leaf-sh
|
|
}
|
|
accounts {
|
|
SH: {
|
|
jetstream: enabled
|
|
users: [{user: u, password: u}]
|
|
}
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
urls: ["leaf://s:[email protected]:%d"]
|
|
account: SH
|
|
}
|
|
]
|
|
}
|
|
`, storeDir, ohubSh2.LeafNode.Port))
|
|
leafSh, _ := RunServerWithConfig(leafShConf)
|
|
defer leafSh.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, hubSh2, 3)
|
|
checkLeafNodeConnected(t, leafSh)
|
|
|
|
ncLeafSh, jsLeafSh := jsClientConnect(t, leafSh, nats.UserInfo("u", "u"))
|
|
defer ncLeafSh.Close()
|
|
|
|
sc := &nats.StreamConfig{
|
|
Name: "leaf-sh",
|
|
Subjects: []string{"leaf2leaf.>"},
|
|
Retention: nats.LimitsPolicy,
|
|
Storage: nats.FileStorage,
|
|
AllowRollup: true,
|
|
AllowDirect: true,
|
|
}
|
|
_, err := jsLeafSh.AddStream(sc)
|
|
require_NoError(t, err)
|
|
|
|
ncLeafJS, jsLeafJS := jsClientConnect(t, leafJS, nats.UserInfo("u", "u"))
|
|
defer ncLeafJS.Close()
|
|
|
|
sc = &nats.StreamConfig{
|
|
Name: "leaf-js",
|
|
Retention: nats.LimitsPolicy,
|
|
Storage: nats.FileStorage,
|
|
AllowRollup: true,
|
|
AllowDirect: true,
|
|
Sources: []*nats.StreamSource{
|
|
{
|
|
Name: "leaf-sh",
|
|
External: &nats.ExternalStream{
|
|
APIPrefix: "$JS.leaf-sh.API",
|
|
DeliverPrefix: "sync.leaf-sh.jspush"},
|
|
},
|
|
},
|
|
}
|
|
_, err = jsLeafJS.AddStream(sc)
|
|
require_NoError(t, err)
|
|
|
|
for range 10 {
|
|
_, err = jsLeafSh.Publish("leaf2leaf.v1.test", []byte("hello"))
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
check := func(js nats.JetStreamContext, stream string) {
|
|
t.Helper()
|
|
checkFor(t, 2*time.Second, 50*time.Millisecond, func() error {
|
|
si, err := js.StreamInfo(stream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n := si.State.Msgs; n != 10 {
|
|
return fmt.Errorf("Expected 10 messages, got %v", n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
check(jsLeafSh, "leaf-sh")
|
|
check(jsLeafJS, "leaf-js")
|
|
|
|
acc := other.GlobalAccount()
|
|
acc.mu.RLock()
|
|
sr := acc.sl.ReverseMatch("sync.leaf-sh.jspush.>")
|
|
acc.mu.RUnlock()
|
|
require_Len(t, len(sr.psubs), 0)
|
|
}
|
|
|
|
func TestLeafNodeConfigureWriteDeadline(t *testing.T) {
|
|
o1, o2 := DefaultOptions(), DefaultOptions()
|
|
|
|
o1.LeafNode.WriteDeadline = 5 * time.Second
|
|
o1.LeafNode.Host = "127.0.0.1"
|
|
o1.LeafNode.Port = -1
|
|
s1 := RunServer(o1)
|
|
defer s1.Shutdown()
|
|
|
|
s1URL, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port))
|
|
o2.Cluster.Name = "somethingelse"
|
|
o2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{s1URL}}}
|
|
s2 := RunServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
s1.mu.RLock()
|
|
defer s1.mu.RUnlock()
|
|
|
|
for _, r := range s1.leafs {
|
|
require_Equal(t, r.out.wdl, 5*time.Second)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeConfigureWriteTimeoutPolicy(t *testing.T) {
|
|
for name, policy := range map[string]WriteTimeoutPolicy{
|
|
"Default": WriteTimeoutPolicyDefault,
|
|
"Retry": WriteTimeoutPolicyRetry,
|
|
"Close": WriteTimeoutPolicyClose,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
o1 := testDefaultOptionsForGateway("B")
|
|
o1.Gateway.WriteTimeout = policy
|
|
s1 := runGatewayServer(o1)
|
|
defer s1.Shutdown()
|
|
|
|
o2 := testGatewayOptionsFromToWithServers(t, "A", "B", s1)
|
|
s2 := runGatewayServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
waitForOutboundGateways(t, s2, 1, time.Second)
|
|
waitForInboundGateways(t, s1, 1, time.Second)
|
|
waitForOutboundGateways(t, s1, 1, time.Second)
|
|
|
|
s1.mu.RLock()
|
|
defer s1.mu.RUnlock()
|
|
|
|
for _, r := range s1.leafs {
|
|
if policy == WriteTimeoutPolicyDefault {
|
|
require_Equal(t, r.out.wtp, WriteTimeoutPolicyRetry)
|
|
} else {
|
|
require_Equal(t, r.out.wtp, policy)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/7441
|
|
func TestLeafNodesBasicTokenAuth(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
authorization {
|
|
token: secret
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
port := ohub.LeafNode.Port
|
|
leafTmpl := `
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [
|
|
{ url: "nats://[email protected]:%d" }
|
|
]
|
|
}
|
|
`
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, leafTmpl, port))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
// Verify that we have only 1 leaf
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
}
|
|
|
|
func TestLeafNodeServiceImportRebuildsRequestInfoHeader(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", pass: "a" }, { user: "leaf", pass: "pwd" } ]
|
|
imports = [ { service: { account: B, subject: "svc" } } ]
|
|
}
|
|
B {
|
|
users = [ { user: "b", pass: "b" } ]
|
|
exports = [ { service: "svc", accounts: [A] } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
authorization {
|
|
account: A
|
|
}
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A {
|
|
users = [ { user: "a", pass: "a" } ]
|
|
}
|
|
}
|
|
leafnodes {
|
|
remotes = [
|
|
{
|
|
url: "nats-leaf://leaf:[email protected]:%d"
|
|
account: A
|
|
}
|
|
]
|
|
}
|
|
`, ohub.LeafNode.Port)))
|
|
leaf, _ := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncSvc := natsConnect(t, hub.ClientURL(), nats.UserInfo("b", "b"))
|
|
defer ncSvc.Close()
|
|
|
|
rcvCh := make(chan *nats.Msg, 1)
|
|
_, err := ncSvc.Subscribe("svc", func(m *nats.Msg) {
|
|
rcvCh <- m
|
|
require_NoError(t, m.Respond([]byte("ok")))
|
|
})
|
|
require_NoError(t, err)
|
|
require_NoError(t, ncSvc.Flush())
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("a", "a"))
|
|
defer ncLeaf.Close()
|
|
|
|
req := nats.NewMsg("svc")
|
|
req.Header = nats.Header{}
|
|
req.Header.Set(ClientInfoHdr, `{"acc":"EVIL","user":"mallory","host":"badhost"}`)
|
|
|
|
_, err = ncLeaf.RequestMsg(req, time.Second)
|
|
require_NoError(t, err)
|
|
|
|
msg := require_ChanRead(t, rcvCh, time.Second)
|
|
hdr := msg.Header.Get(ClientInfoHdr)
|
|
require_NotEqual(t, hdr, _EMPTY_)
|
|
|
|
var ci ClientInfo
|
|
require_NoError(t, json.Unmarshal([]byte(hdr), &ci))
|
|
require_Equal(t, ci.Account, "A")
|
|
require_Equal(t, ci.User, _EMPTY_)
|
|
require_Equal(t, ci.Host, _EMPTY_)
|
|
}
|
|
|
|
func TestLeafNodeNoAccPanicOnLeafSubBeforeConnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
// Default compression is s2_auto. This used to bypass the parser's
|
|
// pre-CONNECT guard because the handshake timeout was tracked outside
|
|
// c.atmr, allowing LS+ through before CONNECT set c.acc.
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Read the INFO.
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Send LS+ without CONNECT first. This should not panic the server.
|
|
_, err = c.Write([]byte("LS+ test\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// The server should close the connection.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 256)
|
|
for {
|
|
_, err = c.Read(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure the server is still running.
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoAccPanicOnLeafUnsubBeforeConnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Read the INFO.
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Send LS- without CONNECT first. This should not panic the server.
|
|
_, err = c.Write([]byte("LS- test\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// The server should close the connection.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 256)
|
|
for {
|
|
_, err = c.Read(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure the server is still running.
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoAccPanicOnLeafErrLoopDetectedBeforeConnect(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`))
|
|
s, opts := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", opts.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Read the INFO.
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Send the loop-detected error before CONNECT. This should not panic the server.
|
|
_, err = c.Write([]byte("-ERR 'Loop detected'\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// The server should close the connection.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 256)
|
|
for {
|
|
_, err = c.Read(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure the server is still running.
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeLeafSubBeforeConnectCompressionEffect(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
compression string
|
|
}{
|
|
{"compression off", CompressionOff},
|
|
{"compression s2_auto", CompressionS2Auto},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Compression.Mode = test.compression
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Send LS+ without CONNECT.
|
|
_, err = c.Write([]byte("LS+ test\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// Read the error response before the connection is closed.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err = br.ReadLine()
|
|
require_NoError(t, err)
|
|
errMsg := string(l)
|
|
|
|
if !strings.Contains(errMsg, "Authorization Violation") {
|
|
t.Fatalf("Expected auth violation error, got %q", errMsg)
|
|
}
|
|
|
|
// Make sure the server is still running.
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeInfoBeforeConnectStillRequiresConnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Compression.Mode = CompressionS2Auto
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.HasPrefix(string(l), "INFO"))
|
|
|
|
// INFO is allowed before CONNECT for compressed leaf-node negotiation.
|
|
_, err = c.Write([]byte("INFO {\"compression\":\"off\"}\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// CONNECT is still required after INFO; other protocols must be rejected.
|
|
_, err = c.Write([]byte("LS+ test\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
require_NoError(t, c.SetReadDeadline(time.Now().Add(2*time.Second)))
|
|
l, _, err = br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(l), "Authorization Violation"))
|
|
}
|
|
|
|
func TestLeafNodeInfoBeforeConnectRejectedWhenCompressionOff(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
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)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.HasPrefix(string(l), "INFO"))
|
|
|
|
_, err = c.Write([]byte("INFO {\"nonce\":\"attacker\"}\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
require_NoError(t, c.SetReadDeadline(time.Now().Add(2*time.Second)))
|
|
l, _, err = br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(l), "Authorization Violation"))
|
|
}
|
|
|
|
func TestLeafNodeInboundInfoDoesNotOverrideNonce(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Compression.Mode = CompressionS2Auto
|
|
s := New(o)
|
|
|
|
nonce := []byte("server-issued-nonce")
|
|
c := &client{
|
|
srv: s,
|
|
kind: LEAF,
|
|
leaf: &leaf{},
|
|
nonce: append([]byte(nil), nonce...),
|
|
}
|
|
c.flags.set(compressionNegotiated)
|
|
|
|
c.processLeafnodeInfo(&Info{Nonce: "attacker-controlled"})
|
|
|
|
require_True(t, bytes.Equal(c.nonce, nonce))
|
|
}
|
|
|
|
func TestLeafNodeSecondInfoBeforeConnectDoesNotPanic(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
secondInfo string
|
|
}{
|
|
{"remote_account", `INFO {"remote_account":"attacker"}`},
|
|
{"connect_info", `INFO {"connect_info":true}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Compression.Mode = CompressionS2Auto
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
br := bufio.NewReader(c)
|
|
require_NoError(t, c.SetReadDeadline(time.Now().Add(2*time.Second)))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.HasPrefix(string(l), "INFO"))
|
|
|
|
// The first INFO is accepted so inbound compression can negotiate.
|
|
_, err = c.Write([]byte("INFO {}\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// A second INFO before CONNECT used to panic the server.
|
|
_, err = c.Write([]byte(test.secondInfo + "\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// Reject the connection, but keep the server alive.
|
|
require_NoError(t, c.SetReadDeadline(time.Now().Add(2*time.Second)))
|
|
l, _, err = br.ReadLine()
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(l), "Authorization Violation"))
|
|
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoAccPanicOnLeafSubBeforeConnectOperatorMode(t *testing.T) {
|
|
// Setup operator JWT-based server with leafnode port.
|
|
// This confirms that even with full operator/JWT auth configured,
|
|
// a raw TCP connection can bypass auth and trigger the panic.
|
|
sysAcc, _ := nkeys.CreateAccount()
|
|
sysAccPub, _ := sysAcc.PublicKey()
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
opub, _ := okp.PublicKey()
|
|
|
|
// Create operator claim with system account.
|
|
oc := jwt.NewOperatorClaims(opub)
|
|
oc.SystemAccount = sysAccPub
|
|
operatorJwt, err := oc.Encode(okp)
|
|
require_NoError(t, err)
|
|
|
|
// Create the system account JWT.
|
|
sysAccClaim := jwt.NewAccountClaims(sysAccPub)
|
|
sysAccJwt, err := sysAccClaim.Encode(okp)
|
|
require_NoError(t, err)
|
|
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: OP
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: MEMORY
|
|
resolver_preload: {
|
|
%s: %s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`, operatorJwt, sysAccPub, sysAccPub, sysAccJwt)))
|
|
s, opts := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", opts.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Read the INFO.
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Send LS+ without CONNECT first, bypassing JWT auth entirely.
|
|
// Without the fix this would panic on nil c.acc dereference.
|
|
_, err = c.Write([]byte("LS+ test\r\n"))
|
|
require_NoError(t, err)
|
|
|
|
// The server should close the connection.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 256)
|
|
for {
|
|
_, err = c.Read(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure the server is still running.
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoAccPanicOnProcessLeafNodeConnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.AuthTimeout = 0.001 // Very short auth timeout (1ms)
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Read the INFO.
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
l, _, err := br.ReadLine()
|
|
require_NoError(t, err)
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Expected INFO, got %q", l)
|
|
}
|
|
|
|
// Wait for the auth timeout to fire, then send CONNECT with cluster.
|
|
// This races with the auth timeout closing the connection, which is
|
|
// the scenario from #7989 where c.acc ends up nil.
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
connect := `CONNECT {"verbose":false,"pedantic":false,"cluster":"test-cluster"}`
|
|
c.Write([]byte(connect + "\r\n"))
|
|
|
|
// The server should close the connection without panicking.
|
|
c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 256)
|
|
for {
|
|
_, err = c.Read(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure the server is still running (not panicked).
|
|
s.mu.Lock()
|
|
shutdown := s.isShuttingDown()
|
|
s.mu.Unlock()
|
|
if shutdown {
|
|
t.Fatal("Server should not have shutdown")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeSpokeConnectAdvisory(t *testing.T) {
|
|
confHub := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
accounts {
|
|
SYS: {users: [{user: sys, password: pwd}]}
|
|
USER: {users: [{user: user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
leafnodes {
|
|
listen: 127.0.0.1:-1
|
|
no_advertise: true
|
|
authorization { timeout: 0.5 }
|
|
}
|
|
`))
|
|
hub, hubOpts := RunServerWithConfig(confHub)
|
|
defer hub.Shutdown()
|
|
hubPort := hubOpts.Port
|
|
hubLeafPort := hubOpts.LeafNode.Port
|
|
|
|
confSpoke := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
accounts {
|
|
SYS: {users: [{user: sys, password: pwd}]}
|
|
USER: {users: [{user: user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
leafnodes {
|
|
reconnect: "50ms"
|
|
remotes: [
|
|
{url: "nats://user:[email protected]:%d", account: USER},
|
|
{url: "nats://sys:[email protected]:%d", account: SYS},
|
|
]
|
|
}
|
|
`, hubLeafPort, hubLeafPort)))
|
|
spoke, spokeOpts := RunServerWithConfig(confSpoke)
|
|
defer spoke.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, hub, 2)
|
|
checkLeafNodeConnectedCount(t, spoke, 2)
|
|
|
|
// Subscribe on spoke's system account for USER account events
|
|
nc := natsConnect(t, fmt.Sprintf("nats://sys:[email protected]:%d", spokeOpts.Port))
|
|
defer nc.Close()
|
|
cSub := natsSubSync(t, nc, "$SYS.ACCOUNT.USER.CONNECT")
|
|
dSub := natsSubSync(t, nc, "$SYS.ACCOUNT.USER.DISCONNECT")
|
|
nc.Flush()
|
|
|
|
// Shut down hub, spoke should emit DISCONNECT
|
|
hub.Shutdown()
|
|
checkLeafNodeConnectedCount(t, spoke, 0)
|
|
msg, err := dSub.NextMsg(time.Second)
|
|
require_NoError(t, err)
|
|
var dm DisconnectEventMsg
|
|
require_NoError(t, json.Unmarshal(msg.Data, &dm))
|
|
require_Equal(t, dm.Client.Kind, "Leafnode")
|
|
|
|
// Restart hub on the same ports so spoke can reconnect
|
|
hubOpts.Port = hubPort
|
|
hubOpts.LeafNode.Port = hubLeafPort
|
|
hub = RunServer(hubOpts)
|
|
defer hub.Shutdown()
|
|
checkLeafNodeConnectedCount(t, spoke, 2)
|
|
|
|
// Spoke should emit CONNECT advisory on reconnect
|
|
msg, err = cSub.NextMsg(time.Second)
|
|
require_NoError(t, err)
|
|
var cm ConnectEventMsg
|
|
require_NoError(t, json.Unmarshal(msg.Data, &cm))
|
|
require_Equal(t, cm.Client.Kind, "Leafnode")
|
|
}
|
|
|
|
func TestLeafNodeRemoteIgnoreGossip(t *testing.T) {
|
|
tmpl := `
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A { users: [{user: "a", password: "pwd"}] }
|
|
B { users: [{user: "b", password: "pwd"}] }
|
|
}
|
|
cluster {
|
|
name: "abc"
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
conf1 := createConfFile(t, fmt.Appendf(nil, tmpl, _EMPTY_))
|
|
sb1, ob1 := RunServerWithConfig(conf1)
|
|
defer sb1.Shutdown()
|
|
|
|
conf2 := createConfFile(t, fmt.Appendf(nil, tmpl,
|
|
fmt.Sprintf("routes: [nats://127.0.0.1:%d]", ob1.Cluster.Port)))
|
|
sb2, _ := RunServerWithConfig(conf2)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
|
|
nc1 := natsConnect(t, sb1.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer nc1.Close()
|
|
natsSubSync(t, nc1, "foo")
|
|
natsFlush(t, nc1)
|
|
|
|
nc2 := natsConnect(t, sb2.ClientURL(), nats.UserInfo("b", "pwd"))
|
|
defer nc2.Close()
|
|
natsSubSync(t, nc2, "bar")
|
|
natsFlush(t, nc2)
|
|
|
|
conf := createConfFile(t, fmt.Appendf(nil, `
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
reconnect_interval: "100ms"
|
|
remotes: [
|
|
{ url: "nats://a:[email protected]:%d", ignore_discovered_servers: true }
|
|
{ url: "nats://b:[email protected]:%d" }
|
|
]
|
|
}
|
|
`, ob1.LeafNode.Port, ob1.LeafNode.Port))
|
|
leaf, _ := RunServerWithConfig(conf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, leaf, 2)
|
|
|
|
checkSubInterest(t, leaf, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, leaf, globalAccountName, "bar", time.Second)
|
|
|
|
// Now shutdown server sb1. The remote that connects to account "A" should
|
|
// fail to reconnect (because it is asked to ignore gossip protocol), while
|
|
// the remote that connects to account "B" should be able to reconnect
|
|
// because it learned about sb2 thanks to gossip protocol (default behavior).
|
|
sb1.Shutdown()
|
|
|
|
// Wait a bit for the disconnect and reconnect interval to make sure there
|
|
// is only one connected remote.
|
|
time.Sleep(150 * time.Millisecond)
|
|
checkLeafNodeConnectedCount(t, leaf, 1)
|
|
|
|
checkSubNoInterest(t, leaf, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, leaf, globalAccountName, "bar", time.Second)
|
|
}
|