a7fec642fc
Most packages should pass the race detector. An exclude list ensures that new packages are automatically tested with -race. Also fix a couple small test races to allow more packages to be tested. Returning readyCh requires a lock because it can be set to nil, and setting it to nil will race without the lock. Move the TestServer.Listening calls around so that they properly guard setting TestServer.l. Otherwise it races. Remove t.Parallel in a small package. The entire package tests run in a few seconds, so t.Parallel does very little. In auto-config, wait for the AutoConfig.run goroutine to stop before calling readPersistedAutoConfig. Without this change there was a data race on reading ac.config.
286 lines
7.1 KiB
Go
286 lines
7.1 KiB
Go
package connect
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/agent"
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
"github.com/hashicorp/consul/testrpc"
|
|
)
|
|
|
|
// Assert io.Closer implementation
|
|
var _ io.Closer = new(Service)
|
|
|
|
func TestService_Name(t *testing.T) {
|
|
ca := connect.TestCA(t, nil)
|
|
s := TestService(t, "web", ca)
|
|
assert.Equal(t, "web", s.Name())
|
|
}
|
|
|
|
func TestService_Dial(t *testing.T) {
|
|
ca := connect.TestCA(t, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
accept bool
|
|
handshake bool
|
|
presentService string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "working",
|
|
accept: true,
|
|
handshake: true,
|
|
presentService: "db",
|
|
wantErr: "",
|
|
},
|
|
{
|
|
name: "tcp connect fail",
|
|
accept: false,
|
|
handshake: false,
|
|
presentService: "db",
|
|
wantErr: "connection refused",
|
|
},
|
|
{
|
|
name: "handshake timeout",
|
|
accept: true,
|
|
handshake: false,
|
|
presentService: "db",
|
|
wantErr: "i/o timeout",
|
|
},
|
|
{
|
|
name: "bad cert",
|
|
accept: true,
|
|
handshake: true,
|
|
presentService: "web",
|
|
wantErr: "peer certificate mismatch",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
s := TestService(t, "web", ca)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(),
|
|
100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
testSvr := NewTestServer(t, tt.presentService, ca)
|
|
testSvr.TimeoutHandshake = !tt.handshake
|
|
|
|
if tt.accept {
|
|
go func() {
|
|
err := testSvr.Serve()
|
|
require.NoError(err)
|
|
}()
|
|
<-testSvr.Listening
|
|
defer testSvr.Close()
|
|
}
|
|
|
|
// Always expect to be connecting to a "DB"
|
|
resolver := &StaticResolver{
|
|
Addr: testSvr.Addr,
|
|
CertURI: connect.TestSpiffeIDService(t, "db"),
|
|
}
|
|
|
|
// All test runs should complete in under 500ms due to the timeout about.
|
|
// Don't wait for whole test run to get stuck.
|
|
testTimeout := 500 * time.Millisecond
|
|
testTimer := time.AfterFunc(testTimeout, func() {
|
|
panic(fmt.Sprintf("test timed out after %s", testTimeout))
|
|
})
|
|
|
|
conn, err := s.Dial(ctx, resolver)
|
|
testTimer.Stop()
|
|
|
|
if tt.wantErr == "" {
|
|
require.NoError(err)
|
|
require.IsType(&tls.Conn{}, conn)
|
|
} else {
|
|
require.Error(err)
|
|
require.Contains(err.Error(), tt.wantErr)
|
|
}
|
|
|
|
if err == nil {
|
|
conn.Close()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestService_ServerTLSConfig(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
a := agent.StartTestAgent(t, agent.TestAgent{Name: "007", Overrides: `
|
|
connect {
|
|
test_ca_leaf_root_change_spread = "1ns"
|
|
}
|
|
`})
|
|
defer a.Shutdown()
|
|
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
|
|
client := a.Client()
|
|
agent := client.Agent()
|
|
|
|
// NewTestAgent setup a CA already by default
|
|
|
|
// Register a local agent service
|
|
reg := &api.AgentServiceRegistration{
|
|
Name: "web",
|
|
Port: 8080,
|
|
}
|
|
err := agent.ServiceRegister(reg)
|
|
require.NoError(err)
|
|
|
|
// Now we should be able to create a service that will eventually get it's TLS
|
|
// all by itself!
|
|
service, err := NewService("web", client)
|
|
require.NoError(err)
|
|
|
|
// Wait for it to be ready
|
|
select {
|
|
case <-service.ReadyWait():
|
|
// continue with test case below
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatalf("timeout waiting for Service.ReadyWait after 1s")
|
|
}
|
|
|
|
tlsCfg := service.ServerTLSConfig()
|
|
|
|
// Sanity check it has a leaf with the right ServiceID and that validates with
|
|
// the given roots.
|
|
require.NotNil(tlsCfg.GetCertificate)
|
|
leaf, err := tlsCfg.GetCertificate(&tls.ClientHelloInfo{})
|
|
require.NoError(err)
|
|
cert, err := x509.ParseCertificate(leaf.Certificate[0])
|
|
require.NoError(err)
|
|
require.Len(cert.URIs, 1)
|
|
require.True(strings.HasSuffix(cert.URIs[0].String(), "/svc/web"))
|
|
|
|
// Verify it as a client would
|
|
err = clientSideVerifier(tlsCfg, leaf.Certificate)
|
|
require.NoError(err)
|
|
|
|
// Now test that rotating the root updates
|
|
{
|
|
// Setup a new generated CA
|
|
connect.TestCAConfigSet(t, a, nil)
|
|
}
|
|
|
|
// After some time, both root and leaves should be different but both should
|
|
// still be correct.
|
|
oldRootSubjects := bytes.Join(tlsCfg.RootCAs.Subjects(), []byte(", "))
|
|
oldLeafSerial := cert.SerialNumber
|
|
oldLeafKeyID := cert.SubjectKeyId
|
|
retry.Run(t, func(r *retry.R) {
|
|
updatedCfg := service.ServerTLSConfig()
|
|
|
|
// Wait until roots are different
|
|
rootSubjects := bytes.Join(updatedCfg.RootCAs.Subjects(), []byte(", "))
|
|
if bytes.Equal(oldRootSubjects, rootSubjects) {
|
|
r.Fatalf("root certificates should have changed, got %s",
|
|
rootSubjects)
|
|
}
|
|
|
|
leaf, err := updatedCfg.GetCertificate(&tls.ClientHelloInfo{})
|
|
r.Check(err)
|
|
cert, err := x509.ParseCertificate(leaf.Certificate[0])
|
|
r.Check(err)
|
|
|
|
if oldLeafSerial.Cmp(cert.SerialNumber) == 0 {
|
|
r.Fatalf("leaf certificate should have changed, got serial %s",
|
|
connect.EncodeSerialNumber(oldLeafSerial))
|
|
}
|
|
if bytes.Equal(oldLeafKeyID, cert.SubjectKeyId) {
|
|
r.Fatalf("leaf should have a different key, got matching SubjectKeyID = %s",
|
|
connect.HexString(oldLeafKeyID))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestService_HTTPClient(t *testing.T) {
|
|
ca := connect.TestCA(t, nil)
|
|
|
|
s := TestService(t, "web", ca)
|
|
|
|
// Run a test HTTP server
|
|
testSvr := NewTestServer(t, "backend", ca)
|
|
defer testSvr.Close()
|
|
go func() {
|
|
err := testSvr.ServeHTTPS(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("Hello, I am Backend"))
|
|
}))
|
|
require.NoError(t, err)
|
|
}()
|
|
<-testSvr.Listening
|
|
|
|
// Still get connection refused some times so retry on those
|
|
retry.Run(t, func(r *retry.R) {
|
|
// Hook the service resolver to avoid needing full agent setup.
|
|
s.httpResolverFromAddr = func(addr string) (Resolver, error) {
|
|
// Require in this goroutine seems to block causing a timeout on the Get.
|
|
//require.Equal("https://backend.service.consul:443", addr)
|
|
return &StaticResolver{
|
|
Addr: testSvr.Addr,
|
|
CertURI: connect.TestSpiffeIDService(t, "backend"),
|
|
}, nil
|
|
}
|
|
|
|
client := s.HTTPClient()
|
|
client.Timeout = 1 * time.Second
|
|
|
|
resp, err := client.Get("https://backend.service.consul/foo")
|
|
r.Check(err)
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
r.Check(err)
|
|
|
|
got := string(bodyBytes)
|
|
want := "Hello, I am Backend"
|
|
if got != want {
|
|
r.Fatalf("got %s, want %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestService_HasDefaultHTTPResolverFromAddr(t *testing.T) {
|
|
|
|
client, err := api.NewClient(api.DefaultConfig())
|
|
require.NoError(t, err)
|
|
|
|
s, err := NewService("foo", client)
|
|
require.NoError(t, err)
|
|
|
|
// Sanity check this is actually set in constructor since we always override
|
|
// it in tests. Full tests of the resolver func are in resolver_test.go
|
|
require.NotNil(t, s.httpResolverFromAddr)
|
|
|
|
fn := s.httpResolverFromAddr
|
|
|
|
expected := &ConsulResolver{
|
|
Client: client,
|
|
Namespace: "default",
|
|
Name: "foo",
|
|
Type: ConsulResolverTypeService,
|
|
}
|
|
got, err := fn("foo.service.consul")
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, got)
|
|
}
|