Skip to content

Commit 74a9a32

Browse files
committed
added optional client keepalives
1 parent 4f600f6 commit 74a9a32

File tree

4 files changed

+117
-57
lines changed

4 files changed

+117
-57
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ and then visit [localhost:3000](http://localhost:3000/), we should see a directo
8080
8181
--key, An optional string to seed the generation of a ECDSA public
8282
and private key pair. All commications will be secured using this
83-
key pair. Share the resulting fingerprint with clients to prevent
84-
man-in-the-middle attacks.
83+
key pair. Share this fingerprint with clients to enable detection
84+
of man-in-the-middle attacks.
8585
8686
--authfile, An optional path to a users.json file. This file should
8787
be an object with users defined like:
@@ -113,7 +113,7 @@ and then visit [localhost:3000](http://localhost:3000/), we should see a directo
113113
114114
server is the URL to the chisel server.
115115
116-
remotes are remote connections tunneled through the server, each of
116+
remotes are remote connections tunnelled through the server, each of
117117
which come in the form:
118118
119119
<local-host>:<local-port>:<remote-host>:<remote-port>
@@ -141,6 +141,12 @@ and then visit [localhost:3000](http://localhost:3000/), we should see a directo
141141
in the form: "<user>:<pass>". These credentials are compared to
142142
the credentials inside the server's --authfile.
143143
144+
--keepalive, An optional keepalive interval. Since the underlying
145+
transport is HTTP, in many instances we'll be traversing through
146+
proxies, often these proxies will close idle connections. You must
147+
specify a time with a unit, for example '30s' or '2m'. Defaults
148+
to '0s' (disabled).
149+
144150
-v, Enable verbose logging
145151
146152
--help, This help text

client/client.go

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,34 @@ import (
1515
"golang.org/x/net/websocket"
1616
)
1717

18+
type Config struct {
19+
shared *chshare.Config
20+
Fingerprint string
21+
Auth string
22+
KeepAlive time.Duration
23+
Server string
24+
Remotes []string
25+
}
26+
1827
type Client struct {
1928
*chshare.Logger
20-
config *chshare.Config
21-
sshConfig *ssh.ClientConfig
22-
proxies []*Proxy
23-
sshConn ssh.Conn
24-
fingerprint string
25-
server string
26-
running bool
27-
runningc chan error
29+
config *Config
30+
sshConfig *ssh.ClientConfig
31+
proxies []*Proxy
32+
sshConn ssh.Conn
33+
server string
34+
running bool
35+
runningc chan error
2836
}
2937

30-
func NewClient(fingerprint, auth, server string, remotes ...string) (*Client, error) {
38+
func NewClient(config *Config) (*Client, error) {
3139

3240
//apply default scheme
33-
if !strings.HasPrefix(server, "http") {
34-
server = "http://" + server
41+
if !strings.HasPrefix(config.Server, "http") {
42+
config.Server = "http://" + config.Server
3543
}
3644

37-
u, err := url.Parse(server)
45+
u, err := url.Parse(config.Server)
3846
if err != nil {
3947
return nil, err
4048
}
@@ -51,34 +59,34 @@ func NewClient(fingerprint, auth, server string, remotes ...string) (*Client, er
5159
//swap to websockets scheme
5260
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
5361

54-
config := &chshare.Config{}
55-
for _, s := range remotes {
62+
shared := &chshare.Config{}
63+
for _, s := range config.Remotes {
5664
r, err := chshare.DecodeRemote(s)
5765
if err != nil {
5866
return nil, fmt.Errorf("Failed to decode remote '%s': %s", s, err)
5967
}
60-
config.Remotes = append(config.Remotes, r)
68+
shared.Remotes = append(shared.Remotes, r)
6169
}
62-
63-
c := &Client{
64-
Logger: chshare.NewLogger("client"),
65-
config: config,
66-
server: u.String(),
67-
fingerprint: fingerprint,
68-
running: true,
69-
runningc: make(chan error, 1),
70+
config.shared = shared
71+
72+
client := &Client{
73+
Logger: chshare.NewLogger("client"),
74+
config: config,
75+
server: u.String(),
76+
running: true,
77+
runningc: make(chan error, 1),
7078
}
7179

72-
user, pass := chshare.ParseAuth(auth)
80+
user, pass := chshare.ParseAuth(config.Auth)
7381

74-
c.sshConfig = &ssh.ClientConfig{
82+
client.sshConfig = &ssh.ClientConfig{
7583
User: user,
7684
Auth: []ssh.AuthMethod{ssh.Password(pass)},
7785
ClientVersion: chshare.ProtocolVersion + "-client",
78-
HostKeyCallback: c.verifyServer,
86+
HostKeyCallback: client.verifyServer,
7987
}
8088

81-
return c, nil
89+
return client, nil
8290
}
8391

8492
//Start then Wait
@@ -88,12 +96,13 @@ func (c *Client) Run() error {
8896
}
8997

9098
func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error {
91-
f := chshare.FingerprintKey(key)
92-
if c.fingerprint != "" && !strings.HasPrefix(f, c.fingerprint) {
93-
return fmt.Errorf("Invalid fingerprint (Got %s)", f)
99+
expect := c.config.Fingerprint
100+
got := chshare.FingerprintKey(key)
101+
if expect != "" && !strings.HasPrefix(got, expect) {
102+
return fmt.Errorf("Invalid fingerprint (%s)", got)
94103
}
95104
//overwrite with complete fingerprint
96-
c.Infof("Fingerprint %s", f)
105+
c.Infof("Fingerprint %s", got)
97106
return nil
98107
}
99108

@@ -106,12 +115,23 @@ func (c *Client) start() {
106115
c.Infof("Connecting to %s\n", c.server)
107116

108117
//prepare proxies
109-
for id, r := range c.config.Remotes {
118+
for id, r := range c.config.shared.Remotes {
110119
proxy := NewProxy(c, id, r)
111120
go proxy.start()
112121
c.proxies = append(c.proxies, proxy)
113122
}
114123

124+
//optional keepalive loop
125+
if c.config.KeepAlive > 0 {
126+
go func() {
127+
for range time.Tick(c.config.KeepAlive) {
128+
if c.sshConn != nil {
129+
c.sshConn.SendRequest("ping", true, nil)
130+
}
131+
}
132+
}()
133+
}
134+
115135
//connection loop!
116136
var connerr error
117137
b := &backoff.Backoff{Max: 5 * time.Minute}
@@ -134,7 +154,8 @@ func (c *Client) start() {
134154
}
135155

136156
sshConn, chans, reqs, err := ssh.NewClientConn(ws, "", c.sshConfig)
137-
//NOTE break -> dont retry on handshake failures
157+
158+
//NOTE: break == dont retry on handshake failures
138159
if err != nil {
139160
if strings.Contains(err.Error(), "unable to authenticate") {
140161
c.Infof("Authentication failed")
@@ -144,16 +165,16 @@ func (c *Client) start() {
144165
}
145166
break
146167
}
147-
conf, _ := chshare.EncodeConfig(c.config)
168+
conf, _ := chshare.EncodeConfig(c.config.shared)
148169
c.Debugf("Sending configurating")
149170
t0 := time.Now()
150-
_, conerr, err := sshConn.SendRequest("config", true, conf)
171+
_, configerr, err := sshConn.SendRequest("config", true, conf)
151172
if err != nil {
152173
c.Infof("Config verification failed")
153174
break
154175
}
155-
if len(conerr) > 0 {
156-
c.Infof(string(conerr))
176+
if len(configerr) > 0 {
177+
c.Infof(string(configerr))
157178
break
158179
}
159180
c.Infof("Connected (Latency %s)", time.Now().Sub(t0))

main.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ func server(args []string) {
126126
*port = "8080"
127127
}
128128

129-
s, err := chserver.NewServer(*key, *authfile, *proxy)
129+
s, err := chserver.NewServer(&chserver.Config{
130+
KeySeed: *key,
131+
AuthFile: *authfile,
132+
Proxy: *proxy,
133+
})
130134
if err != nil {
131135
log.Fatal(err)
132136
}
@@ -171,6 +175,12 @@ var clientHelp = `
171175
--auth, An optional username and password (client authentication)
172176
in the form: "<user>:<pass>". These credentials are compared to
173177
the credentials inside the server's --authfile.
178+
179+
--keepalive, An optional keepalive interval. Since the underlying
180+
transport is HTTP, in many instances we'll be traversing through
181+
proxies, often these proxies will close idle connections. You must
182+
specify a time with a unit, for example '30s' or '2m'. Defaults
183+
to '0s' (disabled).
174184
` + commonHelp
175185

176186
func client(args []string) {
@@ -179,32 +189,34 @@ func client(args []string) {
179189

180190
fingerprint := flags.String("fingerprint", "", "")
181191
auth := flags.String("auth", "", "")
192+
keepalive := flags.Duration("keepalive", 0, "")
182193
verbose := flags.Bool("v", false, "")
183194
flags.Usage = func() {
184195
fmt.Fprintf(os.Stderr, clientHelp)
185196
os.Exit(1)
186197
}
187198
flags.Parse(args)
188-
199+
//pull out options, put back remaining args
189200
args = flags.Args()
190201
if len(args) < 2 {
191202
log.Fatalf("A server and least one remote is required")
192203
}
193204

194-
server := args[0]
195-
remotes := args[1:]
196-
197-
c, err := chclient.NewClient(*fingerprint, *auth, server, remotes...)
205+
c, err := chclient.NewClient(&chclient.Config{
206+
Fingerprint: *fingerprint,
207+
Auth: *auth,
208+
KeepAlive: *keepalive,
209+
Server: args[0],
210+
Remotes: args[1:],
211+
})
198212
if err != nil {
199213
log.Fatal(err)
200214
}
201215

202216
c.Info = true
203217
c.Debug = *verbose
204218

205-
c.Start()
206-
207-
if err = c.Wait(); err != nil {
219+
if err = c.Run(); err != nil {
208220
log.Fatal(err)
209221
}
210222
}

server/server.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@ import (
1313
"golang.org/x/net/websocket"
1414
)
1515

16+
type Config struct {
17+
KeySeed string
18+
AuthFile string
19+
Proxy string
20+
}
21+
1622
type Server struct {
1723
*chshare.Logger
18-
Users chshare.Users
24+
//Users is an empty map of usernames to Users
25+
//It can be optionally initialized using the
26+
//file found at AuthFile
27+
Users chshare.Users
28+
1929
fingerprint string
2030
wsCount int
2131
wsServer websocket.Server
@@ -25,7 +35,7 @@ type Server struct {
2535
sessions map[string]*chshare.User
2636
}
2737

28-
func NewServer(keySeed, authfile, proxy string) (*Server, error) {
38+
func NewServer(config *Config) (*Server, error) {
2939
s := &Server{
3040
Logger: chshare.NewLogger("server"),
3141
wsServer: websocket.Server{},
@@ -35,16 +45,16 @@ func NewServer(keySeed, authfile, proxy string) (*Server, error) {
3545
s.wsServer.Handler = websocket.Handler(s.handleWS)
3646

3747
//parse users, if provided
38-
if authfile != "" {
39-
users, err := chshare.ParseUsers(authfile)
48+
if config.AuthFile != "" {
49+
users, err := chshare.ParseUsers(config.AuthFile)
4050
if err != nil {
4151
return nil, err
4252
}
4353
s.Users = users
4454
}
4555

4656
//generate private key (optionally using seed)
47-
key, _ := chshare.GenerateKey(keySeed)
57+
key, _ := chshare.GenerateKey(config.KeySeed)
4858
//convert into ssh.PrivateKey
4959
private, err := ssh.ParsePrivateKey(key)
5060
if err != nil {
@@ -59,8 +69,8 @@ func NewServer(keySeed, authfile, proxy string) (*Server, error) {
5969
}
6070
s.sshConfig.AddHostKey(private)
6171

62-
if proxy != "" {
63-
u, err := url.Parse(proxy)
72+
if config.Proxy != "" {
73+
u, err := url.Parse(config.Proxy)
6474
if err != nil {
6575
return nil, err
6676
}
@@ -202,7 +212,18 @@ func (s *Server) handleWS(ws *websocket.Conn) {
202212
l := s.Fork("session#%d", id)
203213

204214
l.Debugf("Open")
205-
go ssh.DiscardRequests(reqs)
215+
216+
go func() {
217+
for r := range reqs {
218+
switch r.Type {
219+
case "ping":
220+
r.Reply(true, nil)
221+
default:
222+
l.Debugf("Unknown request: %s", r.Type)
223+
}
224+
}
225+
}()
226+
206227
go chshare.ConnectStreams(l, chans)
207228
sshConn.Wait()
208229
l.Debugf("Close")

0 commit comments

Comments
 (0)