Skip to content

Commit d96b66e

Browse files
committed
merged jpillora#72 which adds reverse tunnelling (thanks @sunshineco!), fixed potential race, USR2 to print go stats, many small cleanups
1 parent a11a3dd commit d96b66e

File tree

13 files changed

+188
-138
lines changed

13 files changed

+188
-138
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# build stage (go modules! farewell GOPATH!)
1+
# build stage
22
FROM golang:alpine AS build-env
33
LABEL maintainer="[email protected]"
44
RUN apk update

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,18 @@ A [demo app](https://chisel-demo.herokuapp.com) on Heroku is running this `chise
4747

4848
```sh
4949
$ chisel server --port $PORT --proxy http://example.com
50-
# listens on $PORT, proxy web requests to 'http://example.com'
50+
# listens on $PORT, proxy web requests to http://example.com
5151
```
5252

5353
This demo app is also running a [simple file server](https://www.npmjs.com/package/serve) on `:3000`, which is normally inaccessible due to Heroku's firewall. However, if we tunnel in with:
5454

5555
```sh
5656
$ chisel client https://chisel-demo.herokuapp.com 3000
57-
# connects to 'https://chisel-demo.herokuapp.com',
57+
# connects to chisel server at https://chisel-demo.herokuapp.com,
5858
# tunnels your localhost:3000 to the server's localhost:3000
5959
```
6060

61-
and then visit [localhost:3000](http://localhost:3000/), we should see a directory listing of the demo app's root. Also, if we visit the [demo app](https://chisel-demo.herokuapp.com) in the browser we should hit the server's default proxy and see a copy of [example.com](http://example.com).
61+
and then visit [localhost:3000](http://localhost:3000/), we should see a directory listing. Also, if we visit the [demo app](https://chisel-demo.herokuapp.com) in the browser we should hit the server's default proxy and see a copy of [example.com](http://example.com).
6262

6363
### Usage
6464

client/client.go

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ type Client struct {
3535
*chshare.Logger
3636
config *Config
3737
sshConfig *ssh.ClientConfig
38-
proxies []*chshare.TCPProxy
3938
sshConn ssh.Conn
4039
httpProxyURL *url.URL
4140
server string
@@ -132,43 +131,39 @@ func (c *Client) Start(ctx context.Context) error {
132131
if c.httpProxyURL != nil {
133132
via = " via " + c.httpProxyURL.String()
134133
}
135-
//prepare proxies
134+
//prepare non-reverse proxies
136135
for i, r := range c.config.shared.Remotes {
137-
if r.Reverse {
138-
continue
139-
}
140-
proxy := chshare.NewTCPProxy(c.Logger, func() ssh.Conn { return c.sshConn }, i, r)
141-
if err := proxy.Start(ctx); err != nil {
142-
return err
136+
if !r.Reverse {
137+
proxy := chshare.NewTCPProxy(c.Logger, func() ssh.Conn { return c.sshConn }, i, r)
138+
if err := proxy.Start(ctx); err != nil {
139+
return err
140+
}
143141
}
144-
c.proxies = append(c.proxies, proxy)
145142
}
146143
c.Infof("Connecting to %s%s\n", c.server, via)
147-
//
148-
go c.loop()
144+
//optional keepalive loop
145+
if c.config.KeepAlive > 0 {
146+
go c.keepAliveLoop()
147+
}
148+
//connection loop
149+
go c.connectionLoop()
149150
return nil
150151
}
151152

152-
func (c *Client) loop() {
153-
//optional keepalive loop
154-
if c.config.KeepAlive > 0 {
155-
go func() {
156-
for range time.Tick(c.config.KeepAlive) {
157-
if c.sshConn != nil {
158-
c.sshConn.SendRequest("ping", true, nil)
159-
} else {
160-
break
161-
}
162-
}
163-
}()
153+
func (c *Client) keepAliveLoop() {
154+
for c.running {
155+
time.Sleep(c.config.KeepAlive)
156+
if c.sshConn != nil {
157+
c.sshConn.SendRequest("ping", true, nil)
158+
}
164159
}
160+
}
161+
162+
func (c *Client) connectionLoop() {
165163
//connection loop!
166164
var connerr error
167165
b := &backoff.Backoff{Max: c.config.MaxRetryInterval}
168-
for {
169-
if !c.running {
170-
break
171-
}
166+
for c.running {
172167
if connerr != nil {
173168
attempt := int(b.Attempt())
174169
maxAttempt := c.config.MaxRetryCount
@@ -272,10 +267,11 @@ func (c *Client) connectStreams(chans <-chan ssh.NewChannel) {
272267
remote := string(ch.ExtraData())
273268
stream, reqs, err := ch.Accept()
274269
if err != nil {
275-
c.Logger.Debugf("Failed to accept stream: %s", err)
270+
c.Debugf("Failed to accept stream: %s", err)
276271
continue
277272
}
278273
go ssh.DiscardRequests(reqs)
279-
go chshare.HandleTCPStream(c.Logger.Fork("tcp#%05d", c.connStats.New()), &c.connStats, stream, remote)
274+
l := c.Logger.Fork("conn#%d", c.connStats.New())
275+
go chshare.HandleTCPStream(l, &c.connStats, stream, remote)
280276
}
281277
}

main.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,17 @@ func main() {
6161
}
6262

6363
var commonHelp = `
64-
--pid Generate pid file in current directory
64+
--pid Generate pid file in current working directory
6565
6666
-v, Enable verbose logging
6767
6868
--help, This help text
6969
70+
Signals:
71+
The chisel process is listening for:
72+
a SIGUSR2 to print process stats, and
73+
a SIGHUP to short-circuit the client reconnect timer
74+
7075
Version:
7176
` + chshare.BuildVersion + `
7277
@@ -181,6 +186,7 @@ func server(args []string) {
181186
if *pid {
182187
generatePidFile()
183188
}
189+
go chshare.GoStats()
184190
if err = s.Run(*host, *port); err != nil {
185191
log.Fatal(err)
186192
}
@@ -219,12 +225,17 @@ var clientHelp = `
219225
5000:socks
220226
R:2222:localhost:22
221227
222-
*When the chisel server has --socks5 enabled, remotes can
228+
When the chisel server has --socks5 enabled, remotes can
223229
specify "socks" in place of remote-host and remote-port.
224230
The default local host and port for a "socks" remote is
225231
127.0.0.1:1080. Connections to this remote will terminate
226232
at the server's internal SOCKS5 proxy.
227233
234+
When the chisel server has --reverse enabled, remotes can
235+
be prefixed with R to denote that they are reversed. That
236+
is, the server will listen and accept connections, and they
237+
will be proxied through the client which specified the remote.
238+
228239
Options:
229240
230241
--fingerprint, A *strongly recommended* fingerprint string
@@ -276,11 +287,9 @@ func client(args []string) {
276287
if len(args) < 2 {
277288
log.Fatalf("A server and least one remote is required")
278289
}
279-
280290
if *auth == "" {
281291
*auth = os.Getenv("AUTH")
282292
}
283-
284293
c, err := chclient.NewClient(&chclient.Config{
285294
Fingerprint: *fingerprint,
286295
Auth: *auth,
@@ -298,6 +307,7 @@ func client(args []string) {
298307
if *pid {
299308
generatePidFile()
300309
}
310+
go chshare.GoStats()
301311
if err = c.Run(); err != nil {
302312
log.Fatal(err)
303313
}

server/handler.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {
1818
//websockets upgrade AND has chisel prefix
1919
upgrade := strings.ToLower(r.Header.Get("Upgrade"))
2020
protocol := r.Header.Get("Sec-WebSocket-Protocol")
21-
if upgrade == "websocket" && protocol == chshare.ProtocolVersion {
22-
s.handleWebsocket(w, r)
23-
return
21+
if upgrade == "websocket" && strings.HasPrefix(protocol, "chisel-") {
22+
if protocol == chshare.ProtocolVersion {
23+
s.handleWebsocket(w, r)
24+
return
25+
}
26+
//print into server logs and silently fall-through
27+
s.Infof("ignored client connection using protocol version %s (expected %s)",
28+
protocol, chshare.ProtocolVersion)
2429
}
2530
//proxy target was provided
2631
if s.reverseProxy != nil {
@@ -62,8 +67,8 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
6267
var user *chshare.User
6368
if s.users.Len() > 0 {
6469
sid := string(sshConn.SessionID())
65-
user = s.sessions[sid]
66-
defer delete(s.sessions, sid)
70+
user, _ = s.sessions.Get(sid)
71+
s.sessions.Del(sid)
6772
}
6873
//verify configuration
6974
clog.Debugf("Verifying configuration")
@@ -76,6 +81,7 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
7681
return
7782
}
7883
failed := func(err error) {
84+
clog.Debugf("Failed: %s", err)
7985
r.Reply(false, []byte(err.Error()))
8086
}
8187
if r.Type != "config" {
@@ -87,6 +93,7 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
8793
failed(s.Errorf("invalid config"))
8894
return
8995
}
96+
//print if client and server versions dont match
9097
if c.Version != chshare.BuildVersion {
9198
v := c.Version
9299
if v == "" {
@@ -95,6 +102,7 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
95102
clog.Infof("Client version (%s) differs from server version (%s)",
96103
v, chshare.BuildVersion)
97104
}
105+
//confirm reverse tunnels are allowed
98106
for _, r := range c.Remotes {
99107
if r.Reverse && !s.reverseOk {
100108
clog.Debugf("Denied reverse port forwarding request, please enable --reverse")
@@ -122,13 +130,12 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
122130
ctx, cancel := context.WithCancel(context.Background())
123131
defer cancel()
124132
for i, r := range c.Remotes {
125-
if !r.Reverse {
126-
continue
127-
}
128-
proxy := chshare.NewTCPProxy(s.Logger, func() ssh.Conn { return sshConn }, i, r)
129-
if err := proxy.Start(ctx); err != nil {
130-
failed(s.Errorf("start '%v' error: %v", r, err))
131-
return
133+
if r.Reverse {
134+
proxy := chshare.NewTCPProxy(s.Logger, func() ssh.Conn { return sshConn }, i, r)
135+
if err := proxy.Start(ctx); err != nil {
136+
failed(s.Errorf("%s", err))
137+
return
138+
}
132139
}
133140
}
134141
//success!
@@ -172,23 +179,22 @@ func (s *Server) handleSSHChannels(clientLog *chshare.Logger, chans <-chan ssh.N
172179
//handle stream type
173180
connID := s.connStats.New()
174181
if socks {
175-
go s.handleSocksStream(clientLog.Fork("socks#%05d", connID), stream)
182+
go s.handleSocksStream(clientLog.Fork("socksconn#%d", connID), stream)
176183
} else {
177-
go chshare.HandleTCPStream(clientLog.Fork(" tcp#%05d", connID), &s.connStats, stream, remote)
184+
go chshare.HandleTCPStream(clientLog.Fork("conn#%d", connID), &s.connStats, stream, remote)
178185
}
179186
}
180187
}
181188

182189
func (s *Server) handleSocksStream(l *chshare.Logger, src io.ReadWriteCloser) {
183190
conn := chshare.NewRWCConn(src)
184-
// conn.SetDeadline(time.Now().Add(30 * time.Second))
185191
s.connStats.Open()
186-
l.Debugf("%s Opening", s.connStats.Status())
192+
l.Debugf("%s Opening", s.connStats)
187193
err := s.socksServer.ServeConn(conn)
188194
s.connStats.Close()
189195
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
190-
l.Debugf("%s Closed (error: %s)", s.connStats.Status(), err)
196+
l.Debugf("%s: Closed (error: %s)", s.connStats, err)
191197
} else {
192-
l.Debugf("%s Closed", s.connStats.Status())
198+
l.Debugf("%s: Closed", s.connStats)
193199
}
194200
}

server/server.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type Server struct {
3636
httpServer *chshare.HTTPServer
3737
reverseProxy *httputil.ReverseProxy
3838
sessCount int32
39-
sessions chshare.Users
39+
sessions *chshare.Users
4040
socksServer *socks5.Server
4141
sshConfig *ssh.ServerConfig
4242
users *chshare.UserIndex
@@ -54,7 +54,7 @@ func NewServer(config *Config) (*Server, error) {
5454
s := &Server{
5555
httpServer: chshare.NewHTTPServer(),
5656
Logger: chshare.NewLogger("server"),
57-
sessions: chshare.Users{},
57+
sessions: chshare.NewUsers(),
5858
reverseOk: config.Reverse,
5959
}
6060
s.Info = true
@@ -64,7 +64,6 @@ func NewServer(config *Config) (*Server, error) {
6464
return nil, err
6565
}
6666
}
67-
6867
if config.Auth != "" {
6968
u := &chshare.User{Addrs: []*regexp.Regexp{chshare.UserAllowAll}}
7069
u.Name, u.Pass = chshare.ParseAuth(config.Auth)
@@ -87,7 +86,6 @@ func NewServer(config *Config) (*Server, error) {
8786
PasswordCallback: s.authUser,
8887
}
8988
s.sshConfig.AddHostKey(private)
90-
9189
//setup reverse proxy
9290
if config.Proxy != "" {
9391
u, err := url.Parse(config.Proxy)
@@ -105,7 +103,6 @@ func NewServer(config *Config) (*Server, error) {
105103
r.Host = u.Host
106104
}
107105
}
108-
109106
//setup socks server (not listening on any port!)
110107
if config.Socks5 {
111108
socksConfig := &socks5.Config{}
@@ -118,9 +115,12 @@ func NewServer(config *Config) (*Server, error) {
118115
if err != nil {
119116
return nil, err
120117
}
121-
s.Infof("SOCKS5 Enabled")
118+
s.Infof("SOCKS5 server enabled")
119+
}
120+
//print when reverse tunnelling is enabled
121+
if config.Reverse {
122+
s.Infof("Reverse tunnelling enabled")
122123
}
123-
124124
return s, nil
125125
}
126126

@@ -168,13 +168,13 @@ func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions
168168
}
169169
// check the user exists and has matching password
170170
n := c.User()
171-
user, found := s.users.GetUser(n)
171+
user, found := s.users.Get(n)
172172
if !found || user.Pass != string(password) {
173173
s.Debugf("Login failed for user: %s", n)
174174
return nil, errors.New("Invalid authentication for username: %s")
175175
}
176176
// insert the user session map
177177
// @note: this should probably have a lock on it given the map isn't thread-safe??
178-
s.sessions[string(c.SessionID())] = user
178+
s.sessions.Set(string(c.SessionID()), user)
179179
return nil, nil
180180
}

share/connstats.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ func (c *ConnStats) Close() {
2222
atomic.AddInt32(&c.open, -1)
2323
}
2424

25-
func (c *ConnStats) Status() string {
25+
func (c *ConnStats) String() string {
2626
return fmt.Sprintf("[%d/%d]", atomic.LoadInt32(&c.open), atomic.LoadInt32(&c.count))
2727
}

share/gostats.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package chshare
2+
3+
import (
4+
"log"
5+
"os"
6+
"os/signal"
7+
"runtime"
8+
"syscall"
9+
"time"
10+
11+
"github.com/jpillora/sizestr"
12+
)
13+
14+
//GoStats prints statistics to
15+
//stdout on SIGUSR2 (posix-only)
16+
func GoStats() {
17+
//silence complaints from windows
18+
const SIGUSR2 = syscall.Signal(0x1f)
19+
time.Sleep(time.Second)
20+
c := make(chan os.Signal, 1)
21+
signal.Notify(c, SIGUSR2)
22+
for range c {
23+
memStats := runtime.MemStats{}
24+
runtime.ReadMemStats(&memStats)
25+
log.Printf("recieved SIGUSR2, go-routines: %d, go-memory-usage: %s",
26+
runtime.NumGoroutine(),
27+
sizestr.ToString(int64(memStats.Alloc)))
28+
}
29+
}

0 commit comments

Comments
 (0)