diff --git a/lib/reversetunnel/localsite.go b/lib/reversetunnel/localsite.go index 5b22b4531db69..fac36244beb66 100644 --- a/lib/reversetunnel/localsite.go +++ b/lib/reversetunnel/localsite.go @@ -42,7 +42,7 @@ import ( "golang.org/x/crypto/ssh" ) -func newlocalSite(srv *server, domainName string, client auth.ClientI, peerClient *proxy.Client) (*localSite, error) { +func newlocalSite(srv *server, domainName string, authServers []string, client auth.ClientI, peerClient *proxy.Client) (*localSite, error) { err := utils.RegisterPrometheusCollectors(localClusterCollectors...) if err != nil { return nil, trace.Wrap(err) @@ -68,6 +68,7 @@ func newlocalSite(srv *server, domainName string, client auth.ClientI, peerClien accessPoint: accessPoint, certificateCache: certificateCache, domainName: domainName, + authServers: authServers, remoteConns: make(map[connKey][]*remoteConn), clock: srv.Clock, log: log.WithFields(log.Fields{ @@ -91,9 +92,10 @@ func newlocalSite(srv *server, domainName string, client auth.ClientI, peerClien // // it implements RemoteSite interface type localSite struct { - log log.FieldLogger - domainName string - srv *server + log log.FieldLogger + domainName string + authServers []string + srv *server // client provides access to the Auth Server API of the local cluster. client auth.ClientI @@ -164,27 +166,18 @@ func (s *localSite) GetLastConnected() time.Time { return s.clock.Now() } -func (s *localSite) DialAuthServer() (conn net.Conn, err error) { - // get list of local auth servers - authServers, err := s.client.GetAuthServers() - if err != nil { - return nil, trace.Wrap(err) - } - - if len(authServers) < 1 { +func (s *localSite) DialAuthServer() (net.Conn, error) { + if len(s.authServers) == 0 { return nil, trace.ConnectionProblem(nil, "no auth servers available") } - // try and dial to one of them, as soon as we are successful, return the net.Conn - for _, authServer := range authServers { - conn, err = net.DialTimeout("tcp", authServer.GetAddr(), apidefaults.DefaultDialTimeout) - if err == nil { - return conn, nil - } + addr := utils.ChooseRandomString(s.authServers) + conn, err := net.DialTimeout("tcp", addr, apidefaults.DefaultDialTimeout) + if err != nil { + return nil, trace.ConnectionProblem(err, "unable to connect to auth server") } - // return the last error - return nil, trace.ConnectionProblem(err, "unable to connect to auth server") + return conn, nil } func (s *localSite) Dial(params DialParams) (net.Conn, error) { diff --git a/lib/reversetunnel/localsite_test.go b/lib/reversetunnel/localsite_test.go index fdcad5090d7b4..92e5d9d4f0286 100644 --- a/lib/reversetunnel/localsite_test.go +++ b/lib/reversetunnel/localsite_test.go @@ -42,7 +42,7 @@ func TestLocalSiteOverlap(t *testing.T) { }, } - site, err := newlocalSite(srv, "clustername", &mockLocalSiteClient{}, nil) + site, err := newlocalSite(srv, "clustername", nil /* authServers */, &mockLocalSiteClient{}, nil /* peerClient */) require.NoError(t, err) nodeID := uuid.NewString() diff --git a/lib/reversetunnel/srv.go b/lib/reversetunnel/srv.go index f8874d00828cd..300fcc2095313 100644 --- a/lib/reversetunnel/srv.go +++ b/lib/reversetunnel/srv.go @@ -118,14 +118,6 @@ type server struct { offlineThreshold time.Duration } -// DirectCluster is used to access cluster directly -type DirectCluster struct { - // Name is a cluster name - Name string - // Client is a client to the cluster - Client auth.ClientI -} - // Config is a reverse tunnel server configuration type Config struct { // ID is the ID of this server proxy @@ -151,8 +143,6 @@ type Config struct { // NewCachingAccessPoint returns new caching access points // per remote cluster NewCachingAccessPoint auth.NewRemoteProxyCachingAccessPoint - // DirectClusters is a list of clusters accessed directly - DirectClusters []DirectCluster // Context is a signalling context Context context.Context // Clock is a clock used in the server, set up to @@ -315,8 +305,6 @@ func NewServer(cfg Config) (Server, error) { srv := &server{ Config: cfg, - localSites: []*localSite{}, - remoteSites: []*remoteSite{}, localAuthClient: cfg.LocalAuthClient, localAccessPoint: cfg.LocalAccessPoint, newAccessPoint: cfg.NewCachingAccessPoint, @@ -329,15 +317,13 @@ func NewServer(cfg Config) (Server, error) { offlineThreshold: offlineThreshold, } - for _, clusterInfo := range cfg.DirectClusters { - cluster, err := newlocalSite(srv, clusterInfo.Name, clusterInfo.Client, srv.PeerClient) - if err != nil { - return nil, trace.Wrap(err) - } - - srv.localSites = append(srv.localSites, cluster) + localSite, err := newlocalSite(srv, cfg.ClusterName, cfg.LocalAuthAddresses, cfg.LocalAuthClient, srv.PeerClient) + if err != nil { + return nil, trace.Wrap(err) } + srv.localSites = append(srv.localSites, localSite) + s, err := sshutils.NewServer( teleport.ComponentReverseTunnelServer, // TODO(klizhentas): improve interface, use struct instead of parameter list diff --git a/lib/reversetunnel/transport.go b/lib/reversetunnel/transport.go index e9d7549031f14..0768e49a5a436 100644 --- a/lib/reversetunnel/transport.go +++ b/lib/reversetunnel/transport.go @@ -197,8 +197,6 @@ func (p *transport) start() { return } - var servers []string - // Parse and extract the dial request from the client. dreq := parseDialReq(req.Payload) if err := dreq.CheckAndSetDefaults(); err != nil { @@ -207,18 +205,22 @@ func (p *transport) start() { } p.log.Debugf("Received out-of-band proxy transport request for %v [%v].", dreq.Address, dreq.ServerID) + // directAddress will hold the address of the node to dial to, if we don't + // have a tunnel for it. + var directAddress string + // Handle special non-resolvable addresses first. switch dreq.Address { // Connect to an Auth Server. case RemoteAuthServer: - if len(p.authServers) <= 0 { + if len(p.authServers) == 0 { p.log.Errorf("connection rejected: no auth servers configured") p.reply(req, false, []byte("no auth servers configured")) return } - servers = p.authServers + directAddress = utils.ChooseRandomString(p.authServers) // Connect to the Kubernetes proxy. case LocalKubernetes: switch p.component { @@ -252,7 +254,7 @@ func (p *transport) start() { return } p.log.Debugf("Forwarding connection to %q", p.kubeDialAddr.Addr) - servers = append(servers, p.kubeDialAddr.Addr) + directAddress = p.kubeDialAddr.Addr } // LocalNode requests are for the single server running in the agent pool. @@ -283,15 +285,16 @@ func (p *transport) start() { // If this is a proxy and not an SSH node, try finding an inbound // tunnel from the SSH node by dreq.ServerID. We'll need to forward // dreq.Address as well. - fallthrough + directAddress = dreq.Address default: - servers = append(servers, dreq.Address) + // Not a special address; could be empty. + directAddress = dreq.Address } // Get a connection to the target address. If a tunnel exists with matching // search names, connection over the tunnel is returned. Otherwise a direct // net.Dial is performed. - conn, useTunnel, err := p.getConn(servers, dreq) + conn, useTunnel, err := p.getConn(directAddress, dreq) if err != nil { errorMessage := fmt.Sprintf("connection rejected: %v", err) fmt.Fprint(p.channel.Stderr(), errorMessage) @@ -368,7 +371,7 @@ func (p *transport) handleChannelRequests(closeContext context.Context, useTunne // getConn checks if the local site holds a connection to the target host, // and if it does, attempts to dial through the tunnel. Otherwise directly // dials to host. -func (p *transport) getConn(servers []string, r *sshutils.DialReq) (net.Conn, bool, error) { +func (p *transport) getConn(addr string, r *sshutils.DialReq) (net.Conn, bool, error) { // This function doesn't attempt to dial if a host with one of the // search names is not registered. It's a fast check. p.log.Debugf("Attempting to dial through tunnel with server ID %q.", r.ServerID) @@ -388,13 +391,13 @@ func (p *transport) getConn(servers []string, r *sshutils.DialReq) (net.Conn, bo } errTun := err - p.log.Debugf("Attempting to dial directly %v.", servers) - conn, err = p.directDial(servers) + p.log.Debugf("Attempting to dial directly %q.", addr) + conn, err = p.directDial(addr) if err != nil { return nil, false, trace.ConnectionProblem(err, "failed dialing through tunnel (%v) or directly (%v)", errTun, err) } - p.log.Debugf("Returning direct dialed connection to %v.", servers) + p.log.Debugf("Returning direct dialed connection to %q.", addr) return conn, false, nil } @@ -438,24 +441,18 @@ func (p *transport) reply(req *ssh.Request, ok bool, msg []byte) { } // directDial attempts to directly dial to the target host. -func (p *transport) directDial(servers []string) (net.Conn, error) { - if len(servers) <= 0 { - return nil, trace.BadParameter("no servers to dial") +func (p *transport) directDial(addr string) (net.Conn, error) { + if addr == "" { + return nil, trace.BadParameter("no address to dial") } - var errors []error - for _, addr := range servers { - dialer := net.Dialer{ - Timeout: apidefaults.DefaultDialTimeout, - } - - conn, err := dialer.DialContext(p.closeContext, "tcp", addr) - if err == nil { - return conn, nil - } - - errors = append(errors, err) + d := net.Dialer{ + Timeout: apidefaults.DefaultDialTimeout, + } + conn, err := d.DialContext(p.closeContext, "tcp", addr) + if err != nil { + return nil, trace.Wrap(err) } - return nil, trace.NewAggregate(errors...) + return conn, nil } diff --git a/lib/service/service.go b/lib/service/service.go index ea91c4b8305a3..ee8f18b2dfb36 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -3085,27 +3085,21 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { NewCachingAccessPoint: process.newLocalCacheForRemoteProxy, NewCachingAccessPointOldProxy: process.newLocalCacheForOldRemoteProxy, Limiter: reverseTunnelLimiter, - DirectClusters: []reversetunnel.DirectCluster{ - { - Name: conn.ServerIdentity.Cert.Extensions[utils.CertExtensionAuthority], - Client: conn.Client, - }, - }, - KeyGen: cfg.Keygen, - Ciphers: cfg.Ciphers, - KEXAlgorithms: cfg.KEXAlgorithms, - MACAlgorithms: cfg.MACAlgorithms, - DataDir: process.Config.DataDir, - PollingPeriod: process.Config.PollingPeriod, - FIPS: cfg.FIPS, - Emitter: streamEmitter, - Log: process.log, - LockWatcher: lockWatcher, - PeerClient: peerClient, - NodeWatcher: nodeWatcher, - CertAuthorityWatcher: caWatcher, - CircuitBreakerConfig: process.Config.CircuitBreakerConfig, - LocalAuthAddresses: utils.NetAddrsToStrings(process.Config.AuthServers), + KeyGen: cfg.Keygen, + Ciphers: cfg.Ciphers, + KEXAlgorithms: cfg.KEXAlgorithms, + MACAlgorithms: cfg.MACAlgorithms, + DataDir: process.Config.DataDir, + PollingPeriod: process.Config.PollingPeriod, + FIPS: cfg.FIPS, + Emitter: streamEmitter, + Log: process.log, + LockWatcher: lockWatcher, + PeerClient: peerClient, + NodeWatcher: nodeWatcher, + CertAuthorityWatcher: caWatcher, + CircuitBreakerConfig: process.Config.CircuitBreakerConfig, + LocalAuthAddresses: utils.NetAddrsToStrings(process.Config.AuthServers), }) if err != nil { return trace.Wrap(err) @@ -3297,7 +3291,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { AuthClient: conn.Client, AccessPoint: accessPoint, HostSigner: conn.ServerIdentity.KeySigner, - LocalCluster: conn.ServerIdentity.Cert.Extensions[utils.CertExtensionAuthority], + LocalCluster: clusterName, KubeDialAddr: utils.DialAddrFromListenAddr(kubeDialAddr(cfg.Proxy, clusterNetworkConfig.GetProxyListenerMode())), ReverseTunnelServer: tsrv, FIPS: process.Config.FIPS, @@ -3498,7 +3492,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { var alpnServer *alpnproxy.Proxy if !cfg.Proxy.DisableTLS && !cfg.Proxy.DisableALPNSNIListener && listeners.web != nil { - authDialerService := alpnproxyauth.NewAuthProxyDialerService(tsrv, accessPoint) + authDialerService := alpnproxyauth.NewAuthProxyDialerService(tsrv, clusterName, utils.NetAddrsToStrings(process.Config.AuthServers)) alpnRouter.Add(alpnproxy.HandlerDecs{ MatchFunc: alpnproxy.MatchByALPNPrefix(string(alpncommon.ProtocolAuth)), HandlerWithConnInfo: authDialerService.HandleConnection, diff --git a/lib/srv/alpnproxy/auth/auth_proxy.go b/lib/srv/alpnproxy/auth/auth_proxy.go index f7b5aae4115b3..25112088b9a63 100644 --- a/lib/srv/alpnproxy/auth/auth_proxy.go +++ b/lib/srv/alpnproxy/auth/auth_proxy.go @@ -18,18 +18,15 @@ package alpnproxyauth import ( "context" - "fmt" "io" - "math/rand" "net" "strings" "github.com/gravitational/trace" - "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/defaults" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/reversetunnel" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/srv/alpnproxy" "github.com/gravitational/teleport/lib/srv/alpnproxy/common" "github.com/gravitational/teleport/lib/utils" @@ -39,16 +36,12 @@ type sitesGetter interface { GetSites() ([]reversetunnel.RemoteSite, error) } -type authGetter interface { - GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error) - GetAuthServers() ([]types.Server, error) -} - // NewAuthProxyDialerService create new instance of AuthProxyDialerService. -func NewAuthProxyDialerService(reverseTunnelServer sitesGetter, accessPoint authGetter) *AuthProxyDialerService { +func NewAuthProxyDialerService(reverseTunnelServer sitesGetter, localClusterName string, authServers []string) *AuthProxyDialerService { return &AuthProxyDialerService{ reverseTunnelServer: reverseTunnelServer, - accessPoint: accessPoint, + localClusterName: localClusterName, + authServers: authServers, } } @@ -56,7 +49,8 @@ func NewAuthProxyDialerService(reverseTunnelServer sitesGetter, accessPoint auth // cluster name and ALPN set to teleport-auth protocol. type AuthProxyDialerService struct { reverseTunnelServer sitesGetter - accessPoint authGetter + localClusterName string + authServers []string } func (s *AuthProxyDialerService) HandleConnection(ctx context.Context, conn net.Conn, connInfo alpnproxy.ConnectionInfo) error { @@ -93,11 +87,7 @@ func getClusterName(info alpnproxy.ConnectionInfo) (string, error) { } func (s *AuthProxyDialerService) dialAuthServer(ctx context.Context, clusterNameFromSNI string) (net.Conn, error) { - clusterName, err := s.accessPoint.GetClusterName() - if err != nil { - return nil, trace.Wrap(err) - } - if clusterName.GetClusterName() == clusterNameFromSNI { + if clusterNameFromSNI == s.localClusterName { return s.dialLocalAuthServer(ctx) } if s.reverseTunnelServer != nil { @@ -107,30 +97,20 @@ func (s *AuthProxyDialerService) dialAuthServer(ctx context.Context, clusterName } func (s *AuthProxyDialerService) dialLocalAuthServer(ctx context.Context) (net.Conn, error) { - authServers, err := s.accessPoint.GetAuthServers() - if err != nil { - return nil, trace.Wrap(err) - } - if len(authServers) == 0 { + if len(s.authServers) == 0 { return nil, trace.NotFound("empty auth servers list") } - var errors []error - - // iterate over the addresses in random order - for len(authServers) > 0 { - l := len(authServers) - authServerIndex := rand.Intn(l) - addr := authServers[authServerIndex].GetAddr() - var d net.Dialer - conn, err := d.DialContext(ctx, "tcp", addr) - if err == nil { - return conn, nil - } - errors = append(errors, fmt.Errorf("%s: %w", addr, err)) - authServers[authServerIndex] = authServers[l-1] - authServers = authServers[:l-1] + + addr := utils.ChooseRandomString(s.authServers) + d := &net.Dialer{ + Timeout: defaults.DefaultDialTimeout, } - return nil, trace.NewAggregate(errors...) + conn, err := d.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, trace.Wrap(err) + } + + return conn, nil } func (s *AuthProxyDialerService) dialRemoteAuthServer(ctx context.Context, clusterName string) (net.Conn, error) { diff --git a/lib/srv/alpnproxy/auth/auth_proxy_test.go b/lib/srv/alpnproxy/auth/auth_proxy_test.go index ff25f26412124..e48160591ad44 100644 --- a/lib/srv/alpnproxy/auth/auth_proxy_test.go +++ b/lib/srv/alpnproxy/auth/auth_proxy_test.go @@ -22,55 +22,44 @@ import ( "testing" "time" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/services" "github.com/stretchr/testify/require" ) -type mockAuthGetter struct { - servers []types.Server -} - -func (m mockAuthGetter) GetClusterName(...services.MarshalOption) (types.ClusterName, error) { - return nil, nil -} - -func (m mockAuthGetter) GetAuthServers() ([]types.Server, error) { - return m.servers, nil -} - func TestDialLocalAuthServerNoServers(t *testing.T) { - s := NewAuthProxyDialerService(nil, mockAuthGetter{servers: []types.Server{}}) + s := NewAuthProxyDialerService(nil /* reverseTunnelServer */, "clustername", nil /* authServers */) _, err := s.dialLocalAuthServer(context.Background()) require.Error(t, err, "dialLocalAuthServer expected to fail") require.Equal(t, "empty auth servers list", err.Error()) } func TestDialLocalAuthServerNoAvailableServers(t *testing.T) { - server1, err := types.NewServer("s1", "auth", types.ServerSpecV2{Addr: "invalid:8000"}) - require.NoError(t, err) - s := NewAuthProxyDialerService(nil, mockAuthGetter{servers: []types.Server{server1}}) - _, err = s.dialLocalAuthServer(context.Background()) + s := NewAuthProxyDialerService(nil /* reverseTunnelServer */, "clustername", []string{"0.0.0.0:3025"}) + _, err := s.dialLocalAuthServer(context.Background()) require.Error(t, err, "dialLocalAuthServer expected to fail") - require.Contains(t, err.Error(), "invalid:8000:") + var netErr *net.OpError + require.ErrorAs(t, err, &netErr) + require.Equal(t, "dial", netErr.Op) + require.Equal(t, "0.0.0.0:3025", netErr.Addr.String()) } func TestDialLocalAuthServerAvailableServers(t *testing.T) { socket, err := net.Listen("tcp", "127.0.0.1:") require.NoError(t, err) - defer socket.Close() - server, err := types.NewServer("s1", "auth", types.ServerSpecV2{Addr: socket.Addr().String()}) - require.NoError(t, err) - servers := []types.Server{server} + t.Cleanup(func() { require.NoError(t, socket.Close()) }) + + authServers := make([]string, 1, 11) + authServers[0] = socket.Addr().String() // multiple invalid servers to minimize chance that we select good one first try - for i := 0; i < 20; i++ { - server, err := types.NewServer("s1", "auth", types.ServerSpecV2{Addr: "invalid2:8000"}) - require.NoError(t, err) - servers = append(servers, server) + for i := 0; i < 10; i++ { + authServers = append(authServers, "0.0.0.0:3025") } - s := NewAuthProxyDialerService(nil, mockAuthGetter{servers: servers}) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - _, err = s.dialLocalAuthServer(ctx) - require.NoError(t, err) + s := NewAuthProxyDialerService(nil /* reverseTunnelServer */, "clustername", authServers) + require.Eventually(t, func() bool { + conn, err := s.dialLocalAuthServer(context.Background()) + if err != nil { + return false + } + conn.Close() + return true + }, 5*time.Second, 10*time.Millisecond) } diff --git a/lib/srv/regular/sshserver_test.go b/lib/srv/regular/sshserver_test.go index 4654d9a99f306..afc6b85c0d65c 100644 --- a/lib/srv/regular/sshserver_test.go +++ b/lib/srv/regular/sshserver_test.go @@ -1151,7 +1151,6 @@ func TestProxyRoundRobin(t *testing.T) { LocalAccessPoint: proxyClient, NewCachingAccessPoint: noCache, NewCachingAccessPointOldProxy: noCache, - DirectClusters: []reversetunnel.DirectCluster{{Name: f.testSrv.ClusterName(), Client: proxyClient}}, DataDir: t.TempDir(), Emitter: proxyClient, Log: logger, @@ -1272,7 +1271,6 @@ func TestProxyDirectAccess(t *testing.T) { LocalAccessPoint: proxyClient, NewCachingAccessPoint: noCache, NewCachingAccessPointOldProxy: noCache, - DirectClusters: []reversetunnel.DirectCluster{{Name: f.testSrv.ClusterName(), Client: proxyClient}}, DataDir: t.TempDir(), Emitter: proxyClient, Log: logger, @@ -1886,7 +1884,6 @@ func TestIgnorePuTTYSimpleChannel(t *testing.T) { LocalAccessPoint: proxyClient, NewCachingAccessPoint: noCache, NewCachingAccessPointOldProxy: noCache, - DirectClusters: []reversetunnel.DirectCluster{{Name: f.testSrv.ClusterName(), Client: proxyClient}}, DataDir: t.TempDir(), Emitter: proxyClient, Log: logger, diff --git a/lib/utils/utils.go b/lib/utils/utils.go index 00cd82799ab75..1ad9d0316b9c0 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/fs" + "math/rand" "net" "net/url" "os" @@ -552,6 +553,18 @@ func RemoveFromSlice(slice []string, values ...string) []string { return output } +// ChooseRandomString returns a random string from the given slice. +func ChooseRandomString(slice []string) string { + switch len(slice) { + case 0: + return "" + case 1: + return slice[0] + default: + return slice[rand.Intn(len(slice))] + } +} + // CheckCertificateFormatFlag checks if the certificate format is valid. func CheckCertificateFormatFlag(s string) (string, error) { switch s { diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 2775b70724105..21636f5991531 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -296,12 +296,12 @@ func newWebSuite(t *testing.T) *WebSuite { LocalAccessPoint: s.proxyClient, Emitter: s.proxyClient, NewCachingAccessPoint: noCache, - DirectClusters: []reversetunnel.DirectCluster{{Name: s.server.ClusterName(), Client: s.proxyClient}}, DataDir: t.TempDir(), LockWatcher: proxyLockWatcher, NodeWatcher: proxyNodeWatcher, CertAuthorityWatcher: caWatcher, CircuitBreakerConfig: breaker.NoopBreakerConfig(), + LocalAuthAddresses: []string{s.server.TLS.Listener.Addr().String()}, }) require.NoError(t, err) s.proxyTunnel = revTunServer @@ -3859,12 +3859,12 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula LocalAccessPoint: client, Emitter: client, NewCachingAccessPoint: noCache, - DirectClusters: []reversetunnel.DirectCluster{{Name: authServer.ClusterName(), Client: client}}, DataDir: t.TempDir(), LockWatcher: proxyLockWatcher, NodeWatcher: proxyNodeWatcher, CertAuthorityWatcher: proxyCAWatcher, CircuitBreakerConfig: breaker.NoopBreakerConfig(), + LocalAuthAddresses: []string{authServer.Listener.Addr().String()}, }) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, revTunServer.Close()) })