diff --git a/internal/proxy/fuse.go b/internal/proxy/fuse.go index 025b1011a..f03125b6f 100644 --- a/internal/proxy/fuse.go +++ b/internal/proxy/fuse.go @@ -23,14 +23,26 @@ import ( "github.com/hanwen/go-fuse/v2/fuse/nodefs" ) +// symlink implements a symbolic link, returning the underlying path when +// Readlink is called. +type symlink struct { + fs.Inode + path string +} + +// Readlink implements fs.NodeReadlinker and returns the symlink's path. +func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) { + return []byte(s.path), fs.OK +} + // readme represents a static read-only text file. type readme struct { fs.Inode } const readmeText = ` -When programs attempt to open files in this directory, a remote connection to -the Cloud SQL instance of the same name will be established. +When applications attempt to open files in this directory, a remote connection +to the Cloud SQL instance of the same name will be established. For example, when you run one of the followg commands, the proxy will initiate a connection to the corresponding Cloud SQL instance, given you have the correct diff --git a/internal/proxy/fuse_test.go b/internal/proxy/fuse_test.go index 67b801108..35e1d851e 100644 --- a/internal/proxy/fuse_test.go +++ b/internal/proxy/fuse_test.go @@ -20,13 +20,15 @@ package proxy_test import ( "context" "io/ioutil" + "net" "os" "path/filepath" "testing" "time" - "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log" + "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql" "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" + "github.com/hanwen/go-fuse/v2/fs" ) func randTmpDir(t interface { @@ -39,48 +41,37 @@ func randTmpDir(t interface { return name } -// tryFunc executes the provided function up to maxCount times, sleeping 100ms -// between attempts. -func tryFunc(f func() error, maxCount int) error { - var errCount int - for { - err := f() - if err == nil { - return nil - } - errCount++ - if errCount == maxCount { - return err +// newTestClient is a convenience function for testing that creates a +// proxy.Client and starts it. The returned cleanup function is also a +// convenience. Callers may choose to ignore it and manually close the client. +func newTestClient(t *testing.T, d cloudsql.Dialer, fuseDir, fuseTempDir string) (*proxy.Client, func()) { + conf := &proxy.Config{FUSEDir: fuseDir, FUSETempDir: fuseTempDir} + c, err := proxy.NewClient(context.Background(), d, testLogger, conf) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + ready := make(chan struct{}) + go c.Serve(context.Background(), func() { close(ready) }) + select { + case <-ready: + case <-time.Tick(5 * time.Second): + t.Fatal("failed to Serve") + } + return c, func() { + if cErr := c.Close(); cErr != nil { + t.Logf("failed to close client: %v", cErr) } - time.Sleep(100 * time.Millisecond) } } -func TestREADME(t *testing.T) { +func TestFUSEREADME(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } - ctx := context.Background() - dir := randTmpDir(t) - conf := &proxy.Config{ - FUSEDir: dir, - FUSETempDir: randTmpDir(t), - } - logger := log.NewStdLogger(os.Stdout, os.Stdout) d := &fakeDialer{} - c, err := proxy.NewClient(ctx, d, logger, conf) - if err != nil { - t.Fatalf("want error = nil, got = %v", err) - } - - ready := make(chan struct{}) - go c.Serve(ctx, func() { close(ready) }) - select { - case <-ready: - case <-time.After(time.Minute): - t.Fatal("proxy.Client failed to start serving") - } + _, cleanup := newTestClient(t, d, dir, randTmpDir(t)) fi, err := os.Stat(dir) if err != nil { @@ -110,13 +101,212 @@ func TestREADME(t *testing.T) { t.Fatalf("expected README data, got no data (dir = %v)", dir) } - if cErr := c.Close(); cErr != nil { - t.Fatalf("c.Close(): %v", cErr) - } + cleanup() // close the client - // verify that c.Close unmounts the FUSE server + // verify that the FUSE server is no longer mounted _, err = ioutil.ReadFile(filepath.Join(dir, "README")) if err == nil { t.Fatal("expected ioutil.Readfile to fail, but it succeeded") } } + +func tryDialUnix(t *testing.T, addr string) net.Conn { + var ( + conn net.Conn + dialErr error + ) + for i := 0; i < 10; i++ { + conn, dialErr = net.Dial("unix", addr) + if conn != nil { + break + } + time.Sleep(100 * time.Millisecond) + } + if dialErr != nil { + t.Fatalf("net.Dial(): %v", dialErr) + } + return conn +} + +func TestFUSEDialInstance(t *testing.T) { + fuseDir := randTmpDir(t) + fuseTempDir := randTmpDir(t) + tcs := []struct { + desc string + wantInstance string + socketPath string + fuseTempDir string + }{ + { + desc: "mysql connections create a Unix socket", + wantInstance: "proj:region:mysql", + socketPath: filepath.Join(fuseDir, "proj:region:mysql"), + fuseTempDir: fuseTempDir, + }, + { + desc: "postgres connections create a directory with a special file", + wantInstance: "proj:region:pg", + socketPath: filepath.Join(fuseDir, "proj:region:pg", ".s.PGSQL.5432"), + fuseTempDir: fuseTempDir, + }, + { + desc: "connecting creates intermediate temp directories", + wantInstance: "proj:region:mysql", + socketPath: filepath.Join(fuseDir, "proj:region:mysql"), + fuseTempDir: filepath.Join(fuseTempDir, "doesntexist"), + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + d := &fakeDialer{} + _, cleanup := newTestClient(t, d, fuseDir, tc.fuseTempDir) + defer cleanup() + + conn := tryDialUnix(t, tc.socketPath) + defer conn.Close() + + var got []string + for i := 0; i < 10; i++ { + got = d.dialedInstances() + if len(got) == 1 { + break + } + time.Sleep(100 * time.Millisecond) + } + if len(got) != 1 { + t.Fatalf("dialed instances len: want = 1, got = %v", got) + } + if want, inst := tc.wantInstance, got[0]; want != inst { + t.Fatalf("instance: want = %v, got = %v", want, inst) + } + + }) + } +} + +func TestFUSEReadDir(t *testing.T) { + fuseDir := randTmpDir(t) + _, cleanup := newTestClient(t, &fakeDialer{}, fuseDir, randTmpDir(t)) + defer cleanup() + + // Initiate a connection so the FUSE server will list it in the dir entries. + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + entries, err := os.ReadDir(fuseDir) + if err != nil { + t.Fatalf("os.ReadDir(): %v", err) + } + // len should be README plus the proj:reg:mysql socket + if got, want := len(entries), 2; got != want { + t.Fatalf("want = %v, got = %v", want, got) + } + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + if names[0] != "README" || names[1] != "proj:reg:mysql" { + t.Fatalf("want = %v, got = %v", []string{"README", "proj:reg:mysql"}, names) + } +} + +func TestFUSEErrors(t *testing.T) { + ctx := context.Background() + d := &fakeDialer{} + c, _ := newTestClient(t, d, randTmpDir(t), randTmpDir(t)) + + // Simulate FUSE file access by invoking Lookup directly to control + // how the socket cache is populated. + _, err := c.Lookup(ctx, "proj:reg:mysql", nil) + if err != fs.OK { + t.Fatalf("proxy.Client.Lookup(): %v", err) + } + + // Close the client to close all open sockets. + if err := c.Close(); err != nil { + t.Fatalf("c.Close(): %v", err) + } + + // Simulate another FUSE file access to directly populated the socket cache. + _, err = c.Lookup(ctx, "proj:reg:mysql", nil) + if err != fs.OK { + t.Fatalf("proxy.Client.Lookup(): %v", err) + } + + // Verify the dialer was called twice, to prove the previous cache entry was + // removed when the socket was closed. + var attempts int + wantAttempts := 2 + for i := 0; i < 10; i++ { + attempts = d.engineVersionAttempts() + if attempts == wantAttempts { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("engine version attempts: want = %v, got = %v", wantAttempts, attempts) +} + +func TestFUSEWithBadInstanceName(t *testing.T) { + fuseDir := randTmpDir(t) + d := &fakeDialer{} + _, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) + defer cleanup() + + _, dialErr := net.Dial("unix", filepath.Join(fuseDir, "notvalid")) + if dialErr == nil { + t.Fatalf("net.Dial() should fail") + } + + if got := d.engineVersionAttempts(); got > 0 { + t.Fatalf("engine version calls: want = 0, got = %v", got) + } +} + +func TestFUSECheckConnections(t *testing.T) { + fuseDir := randTmpDir(t) + d := &fakeDialer{} + c, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) + defer cleanup() + + // first establish a connection to "register" it with the proxy + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + if err := c.CheckConnections(context.Background()); err != nil { + t.Fatalf("c.CheckConnections(): %v", err) + } + + // verify the dialer was invoked twice, once for connect, once for check + // connection + var attempts int + wantAttempts := 2 + for i := 0; i < 10; i++ { + attempts = d.dialAttempts() + if attempts == wantAttempts { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("dial attempts: want = %v, got = %v", wantAttempts, attempts) +} + +func TestFUSEClose(t *testing.T) { + fuseDir := randTmpDir(t) + d := &fakeDialer{} + c, _ := newTestClient(t, d, fuseDir, randTmpDir(t)) + + // first establish a connection to "register" it with the proxy + conn := tryDialUnix(t, filepath.Join(fuseDir, "proj:reg:mysql")) + defer conn.Close() + + // Close the proxy which should close all listeners + if err := c.Close(); err != nil { + t.Fatalf("c.Close(): %v", err) + } + + _, err := net.Dial("unix", filepath.Join(fuseDir, "proj:reg:mysql")) + if err == nil { + t.Fatal("net.Dial() should fail") + } +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 42bae9bd1..7683f1685 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -20,6 +20,8 @@ import ( "io" "net" "os" + "path/filepath" + "regexp" "strings" "sync" "sync/atomic" @@ -34,6 +36,44 @@ import ( "golang.org/x/oauth2" ) +var ( + // Instance connection name is the format :: + // Additionally, we have to support legacy "domain-scoped" projects (e.g. "google.com:PROJECT") + connNameRegex = regexp.MustCompile("([^:]+(:[^:]+)?):([^:]+):([^:]+)") +) + +// connName represents the "instance connection name", in the format +// "project:region:name". Use the "parseConnName" method to initialize this +// struct. +type connName struct { + project string + region string + name string +} + +func (c *connName) String() string { + return fmt.Sprintf("%s:%s:%s", c.project, c.region, c.name) +} + +// parseConnName initializes a new connName struct. +func parseConnName(cn string) (connName, error) { + b := []byte(cn) + m := connNameRegex.FindSubmatch(b) + if m == nil { + return connName{}, fmt.Errorf( + "invalid instance connection name, want = PROJECT:REGION:INSTANCE, got = %v", + cn, + ) + } + + c := connName{ + project: string(m[1]), + region: string(m[3]), + name: string(m[4]), + } + return c, nil +} + // InstanceConnConfig holds the configuration for an individual instance // connection. type InstanceConnConfig struct { @@ -234,6 +274,11 @@ func (c *portConfig) nextDBPort(version string) int { } } +type socketSymlink struct { + socket *socketMount + symlink *symlink +} + // Client proxies connections from a local client to the remote server side // proxy for multiple Cloud SQL instances. type Client struct { @@ -257,9 +302,18 @@ type Client struct { logger cloudsql.Logger // fuseDir specifies the directory where a FUSE server is mounted. The value - // is empty if FUSE is not enabled. - fuseDir string - fuseServer *fuse.Server + // is empty if FUSE is not enabled. The directory holds symlinks to Unix + // domain sockets in the fuseTmpDir. + fuseDir string + fuseTempDir string + // fuseMu protects access to fuseSockets. + fuseMu sync.Mutex + // fuseSockets is a map of instance connection name to socketMount and + // symlink. + fuseSockets map[string]socketSymlink + fuseServerMu sync.Mutex + fuseServer *fuse.Server + fuseWg sync.WaitGroup // Inode adds support for FUSE operations. fs.Inode @@ -289,7 +343,12 @@ func NewClient(ctx context.Context, d cloudsql.Dialer, l cloudsql.Logger, conf * } if conf.FUSEDir != "" { + if err := os.MkdirAll(conf.FUSETempDir, 0777); err != nil { + return nil, err + } c.fuseDir = conf.FUSEDir + c.fuseTempDir = conf.FUSETempDir + c.fuseSockets = map[string]socketSymlink{} return c, nil } @@ -324,10 +383,24 @@ func NewClient(ctx context.Context, d cloudsql.Dialer, l cloudsql.Logger, conf * return c, nil } +// Readdir returns a list of all active Unix sockets in addition to the README. func (c *Client) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { entries := []fuse.DirEntry{ {Name: "README", Mode: 0555 | fuse.S_IFREG}, } + var active []string + c.fuseMu.Lock() + for k := range c.fuseSockets { + active = append(active, k) + } + c.fuseMu.Unlock() + + for _, a := range active { + entries = append(entries, fuse.DirEntry{ + Name: a, + Mode: 0777 | syscall.S_IFSOCK, + }) + } return fs.NewListDirStream(entries), fs.OK } @@ -340,7 +413,54 @@ func (c *Client) Lookup(ctx context.Context, instance string, out *fuse.EntryOut if instance == "README" { return c.NewInode(ctx, &readme{}, fs.StableAttr{}), fs.OK } - return nil, syscall.ENOENT + + if _, err := parseConnName(instance); err != nil { + return nil, syscall.ENOENT + } + + c.fuseMu.Lock() + defer c.fuseMu.Unlock() + if l, ok := c.fuseSockets[instance]; ok { + return l.symlink.EmbeddedInode(), fs.OK + } + + version, err := c.dialer.EngineVersion(ctx, instance) + if err != nil { + c.logger.Errorf("could not resolve version for %q: %v", instance, err) + return nil, syscall.ENOENT + } + + s, err := newSocketMount( + ctx, &Config{UnixSocket: c.fuseTempDir}, + nil, InstanceConnConfig{Name: instance}, version, + ) + if err != nil { + c.logger.Errorf("could not create socket for %q: %v", instance, err) + return nil, syscall.ENOENT + } + + c.fuseWg.Add(1) + go func() { + defer c.fuseWg.Done() + sErr := c.serveSocketMount(ctx, s) + if sErr != nil { + c.fuseMu.Lock() + delete(c.fuseSockets, instance) + c.fuseMu.Unlock() + } + }() + + // Return a symlink that points to the actual Unix socket within the + // temporary directory. For Postgres, return a symlink that points to the + // directory which holds the ".s.PGSQL.5432" Unix socket. + sl := &symlink{path: filepath.Join(c.fuseTempDir, instance)} + c.fuseSockets[instance] = socketSymlink{ + socket: s, + symlink: sl, + } + return c.NewInode(ctx, sl, fs.StableAttr{ + Mode: 0777 | fuse.S_IFLNK}, + ), fs.OK } // CheckConnections dials each registered instance and reports any errors that @@ -349,8 +469,17 @@ func (c *Client) CheckConnections(ctx context.Context) error { var ( wg sync.WaitGroup errCh = make(chan error, len(c.mnts)) + mnts = c.mnts ) - for _, mnt := range c.mnts { + if c.fuseDir != "" { + mnts = []*socketMount{} + c.fuseMu.Lock() + for _, m := range c.fuseSockets { + mnts = append(mnts, m.socket) + } + c.fuseMu.Unlock() + } + for _, mnt := range mnts { wg.Add(1) go func(m *socketMount) { defer wg.Done() @@ -401,7 +530,9 @@ func (c *Client) Serve(ctx context.Context, notify func()) error { if err != nil { return fmt.Errorf("FUSE mount failed: %q: %v", c.fuseDir, err) } + c.fuseServerMu.Lock() c.fuseServer = srv + c.fuseServerMu.Unlock() notify() <-ctx.Done() return ctx.Err() @@ -447,19 +578,35 @@ func (m MultiErr) Error() string { // Close triggers the proxyClient to shutdown. func (c *Client) Close() error { + mnts := c.mnts + + c.fuseServerMu.Lock() + hasFuseServer := c.fuseServer != nil + c.fuseServerMu.Unlock() + var mErr MultiErr - if c.fuseServer != nil { + if hasFuseServer { if err := c.fuseServer.Unmount(); err != nil { mErr = append(mErr, err) } + mnts = []*socketMount{} + c.fuseMu.Lock() + for _, m := range c.fuseSockets { + mnts = append(mnts, m.socket) + } + c.fuseMu.Unlock() } + // First, close all open socket listeners to prevent additional connections. - for _, m := range c.mnts { + for _, m := range mnts { err := m.Close() if err != nil { mErr = append(mErr, err) } } + if hasFuseServer { + c.fuseWg.Wait() + } // Next, close the dialer to prevent any additional refreshes. cErr := c.dialer.Close() if cErr != nil { @@ -541,9 +688,10 @@ func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { // socketMount is a tcp/unix socket that listens for a Cloud SQL instance. type socketMount struct { + fs.Inode inst string - dialOpts []cloudsqlconn.DialOption listener net.Listener + dialOpts []cloudsqlconn.DialOption } func newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst InstanceConnConfig, version string) (*socketMount, error) { diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index cfe25f591..f7fe6b78b 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -32,9 +32,13 @@ import ( "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" ) +var testLogger = log.NewStdLogger(os.Stdout, os.Stdout) + type fakeDialer struct { - mu sync.Mutex - dialCount int + mu sync.Mutex + dialCount int + engineVersionCount int + instances []string } func (*fakeDialer) Close() error { @@ -47,15 +51,31 @@ func (f *fakeDialer) dialAttempts() int { return f.dialCount } +func (f *fakeDialer) engineVersionAttempts() int { + f.mu.Lock() + defer f.mu.Unlock() + return f.engineVersionCount +} + +func (f *fakeDialer) dialedInstances() []string { + f.mu.Lock() + defer f.mu.Unlock() + return append([]string{}, f.instances...) +} + func (f *fakeDialer) Dial(ctx context.Context, inst string, opts ...cloudsqlconn.DialOption) (net.Conn, error) { f.mu.Lock() defer f.mu.Unlock() f.dialCount++ + f.instances = append(f.instances, inst) c1, _ := net.Pipe() return c1, nil } -func (*fakeDialer) EngineVersion(_ context.Context, inst string) (string, error) { +func (f *fakeDialer) EngineVersion(_ context.Context, inst string) (string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.engineVersionCount++ switch { case strings.Contains(inst, "pg"): return "POSTGRES_14", nil @@ -242,8 +262,7 @@ func TestClientInitialization(t *testing.T) { for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(ctx, &fakeDialer{}, logger, tc.in) + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, tc.in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } @@ -287,8 +306,7 @@ func TestClientLimitsMaxConnections(t *testing.T) { }, MaxConnections: 1, } - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(context.Background(), d, logger, in) + c, err := proxy.NewClient(context.Background(), d, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -350,7 +368,6 @@ func tryTCPDial(t *testing.T, addr string) net.Conn { } func TestClientCloseWaitsForActiveConnections(t *testing.T) { - logger := log.NewStdLogger(os.Stdout, os.Stdout) in := &proxy.Config{ Addr: "127.0.0.1", Port: 5000, @@ -359,7 +376,7 @@ func TestClientCloseWaitsForActiveConnections(t *testing.T) { }, WaitOnClose: 5 * time.Second, } - c, err := proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -389,8 +406,7 @@ func TestClientClosesCleanly(t *testing.T) { {Name: "proj:reg:inst"}, }, } - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error want = nil, got = %v", err) } @@ -412,8 +428,7 @@ func TestClosesWithError(t *testing.T) { {Name: "proj:reg:inst"}, }, } - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(context.Background(), &errorDialer{}, logger, in) + c, err := proxy.NewClient(context.Background(), &errorDialer{}, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error want = nil, got = %v", err) } @@ -469,14 +484,13 @@ func TestClientInitializationWorksRepeatedly(t *testing.T) { }, } - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(ctx, &fakeDialer{}, logger, in) + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } c.Close() - c, err = proxy.NewClient(ctx, &fakeDialer{}, logger, in) + c, err = proxy.NewClient(ctx, &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } @@ -490,8 +504,7 @@ func TestClientNotifiesCallerOnServe(t *testing.T) { {Name: "proj:region:pg"}, }, } - logger := log.NewStdLogger(os.Stdout, os.Stdout) - c, err := proxy.NewClient(ctx, &fakeDialer{}, logger, in) + c, err := proxy.NewClient(ctx, &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("want error = nil, got = %v", err) } @@ -515,7 +528,6 @@ func TestClientNotifiesCallerOnServe(t *testing.T) { } func TestClientConnCount(t *testing.T) { - logger := log.NewStdLogger(os.Stdout, os.Stdout) in := &proxy.Config{ Addr: "127.0.0.1", Port: 5000, @@ -525,7 +537,7 @@ func TestClientConnCount(t *testing.T) { MaxConnections: 10, } - c, err := proxy.NewClient(context.Background(), &fakeDialer{}, logger, in) + c, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -558,7 +570,6 @@ func TestClientConnCount(t *testing.T) { } func TestCheckConnections(t *testing.T) { - logger := log.NewStdLogger(os.Stdout, os.Stdout) in := &proxy.Config{ Addr: "127.0.0.1", Port: 5000, @@ -567,7 +578,7 @@ func TestCheckConnections(t *testing.T) { }, } d := &fakeDialer{} - c, err := proxy.NewClient(context.Background(), d, logger, in) + c, err := proxy.NewClient(context.Background(), d, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) } @@ -591,7 +602,7 @@ func TestCheckConnections(t *testing.T) { }, } ed := &errorDialer{} - c, err = proxy.NewClient(context.Background(), ed, logger, in) + c, err = proxy.NewClient(context.Background(), ed, testLogger, in) if err != nil { t.Fatalf("proxy.NewClient error: %v", err) }