Skip to content

Commit 324c272

Browse files
committed
split off ssh.Client from deployment.SSHTunnel
Signed-off-by: dmitriy kalinin <dkalinin@pivotal.io>
1 parent d642a96 commit 324c272

7 files changed

Lines changed: 351 additions & 235 deletions

File tree

deployment/sshtunnel/ssh_tunnel.go

Lines changed: 28 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ import (
44
"fmt"
55
"io"
66
"net"
7-
"strings"
8-
"time"
9-
10-
"golang.org/x/crypto/ssh"
117

128
bosherr "github.com/cloudfoundry/bosh-utils/errors"
139
boshlog "github.com/cloudfoundry/bosh-utils/logger"
14-
"github.com/pivotal-golang/clock"
10+
11+
boshssh "github.com/cloudfoundry/bosh-cli/ssh"
1512
)
1613

1714
type SSHTunnel interface {
@@ -20,104 +17,49 @@ type SSHTunnel interface {
2017
}
2118

2219
type sshTunnel struct {
23-
connectionRefusedTimeout time.Duration
24-
authFailureTimeout time.Duration
25-
timeService clock.Clock
26-
startDialDelay time.Duration
27-
options Options
28-
remoteListener net.Listener
29-
logger boshlog.Logger
30-
logTag string
31-
}
32-
33-
func (s *sshTunnel) Start(readyErrCh chan<- error, errCh chan<- error) {
34-
authMethods := []ssh.AuthMethod{}
35-
36-
if s.options.PrivateKey != "" {
37-
signer, err := ssh.ParsePrivateKey([]byte(s.options.PrivateKey))
38-
if err != nil {
39-
readyErrCh <- bosherr.WrapErrorf(err, "Parsing private key '%s'", s.options.PrivateKey)
40-
return
41-
}
42-
43-
authMethods = append(authMethods, ssh.PublicKeys(signer))
44-
}
45-
46-
if s.options.Password != "" {
47-
s.logger.Debug(s.logTag, "Adding password auth method to ssh tunnel config")
48-
49-
keyboardInteractiveChallenge := func(
50-
user,
51-
instruction string,
52-
questions []string,
53-
echos []bool,
54-
) (answers []string, err error) {
55-
if len(questions) == 0 {
56-
return []string{}, nil
57-
}
58-
return []string{s.options.Password}, nil
59-
}
60-
authMethods = append(authMethods, ssh.KeyboardInteractive(keyboardInteractiveChallenge))
61-
authMethods = append(authMethods, ssh.Password(s.options.Password))
62-
}
63-
64-
sshConfig := &ssh.ClientConfig{
65-
User: s.options.User,
66-
Auth: authMethods,
67-
}
68-
69-
s.logger.Debug(s.logTag, "Dialing remote server at %s:%d", s.options.Host, s.options.Port)
70-
remoteAddr := fmt.Sprintf("%s:%d", s.options.Host, s.options.Port)
20+
client boshssh.Client
7121

72-
retryStrategy := &SSHRetryStrategy{
73-
TimeService: s.timeService,
74-
ConnectionRefusedTimeout: s.connectionRefusedTimeout,
75-
AuthFailureTimeout: s.authFailureTimeout,
76-
}
77-
78-
var conn *ssh.Client
79-
var err error
80-
for i := 0; ; i++ {
81-
s.logger.Debug(s.logTag, "Making attempt #%d", i)
82-
conn, err = ssh.Dial("tcp", remoteAddr, sshConfig)
83-
84-
if err == nil {
85-
break
86-
}
22+
localForwardPort int
23+
remoteForwardPort int
8724

88-
if !retryStrategy.IsRetryable(err) {
89-
readyErrCh <- bosherr.WrapError(err, "Failed to connect to remote server")
90-
return
91-
}
25+
remoteListener net.Listener
9226

93-
s.logger.Debug(s.logTag, "Attempt failed #%d: Dialing remote server: %s", i, err.Error())
27+
logTag string
28+
logger boshlog.Logger
29+
}
9430

95-
time.Sleep(s.startDialDelay)
31+
func (s *sshTunnel) Start(readyErrCh chan<- error, errCh chan<- error) {
32+
err := s.client.Start()
33+
if err != nil {
34+
readyErrCh <- bosherr.WrapError(err, "Starting SSH tunnel")
35+
return
9636
}
9737

98-
remoteListenAddr := fmt.Sprintf("127.0.0.1:%d", s.options.RemoteForwardPort)
38+
remoteListenAddr := fmt.Sprintf("127.0.0.1:%d", s.remoteForwardPort)
9939
s.logger.Debug(s.logTag, "Listening on remote server %s", remoteListenAddr)
100-
s.remoteListener, err = conn.Listen("tcp", remoteListenAddr)
40+
s.remoteListener, err = s.client.Listen("tcp", remoteListenAddr)
10141
if err != nil {
10242
readyErrCh <- bosherr.WrapError(err, "Listening on remote server")
10343
return
10444
}
10545

10646
readyErrCh <- nil
47+
10748
for {
10849
remoteConn, err := s.remoteListener.Accept()
10950
s.logger.Debug(s.logTag, "Received connection")
11051
if err != nil {
11152
errCh <- bosherr.WrapError(err, "Accepting connection on remote server")
11253
}
54+
11355
defer func() {
11456
if err = remoteConn.Close(); err != nil {
11557
s.logger.Warn(s.logTag, "Failed to close remote listener connection: %s", err.Error())
11658
}
11759
}()
11860

11961
s.logger.Debug(s.logTag, "Dialing local server")
120-
localDialAddr := fmt.Sprintf("127.0.0.1:%d", s.options.LocalForwardPort)
62+
localDialAddr := fmt.Sprintf("127.0.0.1:%d", s.localForwardPort)
12163
localConn, err := net.Dial("tcp", localDialAddr)
12264
if err != nil {
12365
errCh <- bosherr.WrapError(err, "Dialing local server")
@@ -126,25 +68,31 @@ func (s *sshTunnel) Start(readyErrCh chan<- error, errCh chan<- error) {
12668

12769
go func() {
12870
bytesNum, err := io.Copy(remoteConn, localConn)
71+
12972
defer func() {
13073
if err = localConn.Close(); err != nil {
13174
s.logger.Warn(s.logTag, "Failed to close local dial connection: %s", err.Error())
13275
}
13376
}()
77+
13478
s.logger.Debug(s.logTag, "Copying bytes from local to remote %d", bytesNum)
79+
13580
if err != nil {
13681
errCh <- bosherr.WrapError(err, "Copying bytes from local to remote")
13782
}
13883
}()
13984

14085
go func() {
14186
bytesNum, err := io.Copy(localConn, remoteConn)
87+
14288
defer func() {
14389
if err = localConn.Close(); err != nil {
14490
s.logger.Warn(s.logTag, "Failed to close local dial connection: %s", err.Error())
14591
}
14692
}()
93+
14794
s.logger.Debug(s.logTag, "Copying bytes from remote to local %d", bytesNum)
95+
14896
if err != nil {
14997
errCh <- bosherr.WrapError(err, "Copying bytes from remote to local")
15098
}
@@ -153,39 +101,8 @@ func (s *sshTunnel) Start(readyErrCh chan<- error, errCh chan<- error) {
153101
}
154102

155103
func (s *sshTunnel) Stop() error {
156-
if s.remoteListener == nil {
157-
return nil
104+
if s.remoteListener != nil {
105+
return s.remoteListener.Close()
158106
}
159-
160-
return s.remoteListener.Close()
161-
}
162-
163-
type SSHRetryStrategy struct {
164-
ConnectionRefusedTimeout time.Duration
165-
AuthFailureTimeout time.Duration
166-
TimeService clock.Clock
167-
168-
initialized bool
169-
startTime time.Time
170-
authStartTime time.Time
171-
}
172-
173-
func (s *SSHRetryStrategy) IsRetryable(err error) bool {
174-
now := s.TimeService.Now()
175-
if !s.initialized {
176-
s.startTime = now
177-
s.authStartTime = now
178-
s.initialized = true
179-
}
180-
181-
if strings.Contains(err.Error(), "no common algorithms") {
182-
return false
183-
}
184-
185-
if strings.Contains(err.Error(), "unable to authenticate") {
186-
return now.Before(s.authStartTime.Add(s.AuthFailureTimeout))
187-
}
188-
189-
s.authStartTime = now
190-
return now.Before(s.startTime.Add(s.ConnectionRefusedTimeout))
107+
return nil
191108
}
Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package sshtunnel
22

33
import (
4-
"time"
5-
4+
boshssh "github.com/cloudfoundry/bosh-cli/ssh"
65
boshlog "github.com/cloudfoundry/bosh-utils/logger"
7-
"github.com/pivotal-golang/clock"
86
)
97

108
type Options struct {
11-
Host string
12-
Port int
9+
Host string
10+
Port int
11+
1312
User string
14-
PrivateKey string
1513
Password string
14+
PrivateKey string
1615

1716
LocalForwardPort int
1817
RemoteForwardPort int
@@ -31,20 +30,30 @@ type factory struct {
3130
}
3231

3332
func NewFactory(logger boshlog.Logger) Factory {
34-
return &factory{
35-
logger: logger,
36-
}
33+
return &factory{logger: logger}
3734
}
3835

39-
func (s *factory) NewSSHTunnel(options Options) SSHTunnel {
40-
timeService := clock.NewClock()
41-
return &sshTunnel{
42-
connectionRefusedTimeout: 5 * time.Minute,
43-
authFailureTimeout: 2 * time.Minute,
44-
startDialDelay: 500 * time.Millisecond,
45-
timeService: timeService,
46-
options: options,
47-
logger: s.logger,
48-
logTag: "sshTunnel",
36+
func (f *factory) NewSSHTunnel(opts Options) SSHTunnel {
37+
clientFactory := boshssh.NewClientFactory(f.logger)
38+
39+
clientOpts := boshssh.ClientOpts{
40+
Host: opts.Host,
41+
Port: opts.Port,
42+
43+
User: opts.User,
44+
Password: opts.Password,
45+
PrivateKey: opts.PrivateKey,
46+
}
47+
48+
tunnel := &sshTunnel{
49+
client: clientFactory.New(clientOpts),
50+
51+
localForwardPort: opts.LocalForwardPort,
52+
remoteForwardPort: opts.RemoteForwardPort,
53+
54+
logTag: "sshTunnel",
55+
logger: f.logger,
4956
}
57+
58+
return tunnel
5059
}

deployment/sshtunnel/ssh_tunnel_test.go

Lines changed: 0 additions & 93 deletions
This file was deleted.

deployment/sshtunnel/sshtunnel_suite_test.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)