Skip to content

Commit cbabf54

Browse files
authored
Remote forwarding (#88)
* context: fixed documentation to be more specific about ContextKeyConn being the key for a gossh.ServerConn Signed-off-by: Jeff Lindsay <[email protected]> * server: fixes handler setup, changed to interface based handlers, added global request handler map * tcpip: working remote forwarding Signed-off-by: Jeff Lindsay <[email protected]> * context: docs typo Signed-off-by: Jeff Lindsay <[email protected]> * session: always reply to unblock clients trying something Signed-off-by: Jeff Lindsay <[email protected]> * tcpip: stop listening when ssh clients disconnect Signed-off-by: Jeff Lindsay <[email protected]> * Remote forwarding (#87) * Update generateSigner key size to 2048 (#62) Fixes #58 * Add syntax highlighting to readme (#67) * small api updates (#69) These updates make it easier to implement and pass custom Session and Context implementations No compatibilty breaking, all tests pass * Move channelHandlers to avoid data race (#59) * Update tests to work with go 1.10+ (#73) Fixes #72 * Update shutdown to use a WaitGroup rather than sleeping (#74) * Fix race condition in TestServerClose (#75) In test server close, 3 things need to happen in order: - Client session start - Server.Close - Client session exit (With io.EOF) This fix ensures the client won't do anything until after the call to close which ensure's we'll get io.EOF rather than a different error. * Update circleci config to test multiple go versions * Update CircleCI config to test 1.9 and the latest The x/crypto/ssh library dropped support go < 1.9 as that's the first version to have the math/bits library. golang/crypto@83c378c * Wait for connections to finish when shutting down PR #74 introduced a WaitGroup for listeners, but it doesn't wait for open connections before closing the server. This patch waits until all conns are closed before returning from Shutdown. * Support port forwarding of literal IPv6 addresses (#85) * Support port forwarding of literal IPv6 addresses To disambiguate between colons as host:port separators and as IPv6 address separators, literal IPv6 addresses use square brackets around the address (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers). So host ::1, port 22 is written as [::1]:22, and therefore a simple concatenation of host, colon, and port doesn't work. Fortunately net.JoinHostPort already implements this functionality, so with a bit of type gymnastics we can generate dest in an IPv6-safe way. * Support port forwarding of literal IPv6 addresses To disambiguate between colons as host:port separators and as IPv6 address separators, literal IPv6 addresses use square brackets around the address (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers). So host ::1, port 22 is written as [::1]:22, and therefore a simple concatenation of host, colon, and port doesn't work. Fortunately net.JoinHostPort already implements this functionality, so with a bit of type gymnastics we can generate dest in an IPv6-safe way. * Reverse port forwarding callback added * garbage removed
1 parent c072a10 commit cbabf54

File tree

8 files changed

+218
-18
lines changed

8 files changed

+218
-18
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"log"
6+
7+
"github.com/gliderlabs/ssh"
8+
)
9+
10+
func main() {
11+
12+
log.Println("starting ssh server on port 2222...")
13+
14+
server := ssh.Server{
15+
LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool {
16+
log.Println("Accepted forward", dhost, dport)
17+
return true
18+
}),
19+
Addr: ":2222",
20+
Handler: ssh.Handler(func(s ssh.Session) {
21+
io.WriteString(s, "Remote forwarding available...\n")
22+
select {}
23+
}),
24+
ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool {
25+
log.Println("attempt to bind", host, port, "granted")
26+
return true
27+
}),
28+
}
29+
30+
log.Fatal(server.ListenAndServe())
31+
}

context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ var (
4848
ContextKeyServer = &contextKey{"ssh-server"}
4949

5050
// ContextKeyConn is a context key for use with Contexts in this package.
51-
// The associated value will be of type gossh.Conn.
51+
// The associated value will be of type gossh.ServerConn.
5252
ContextKeyConn = &contextKey{"ssh-conn"}
5353

5454
// ContextKeyPublicKey is a context key for use with Contexts in this package.

doc.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/*
2-
32
Package ssh wraps the crypto/ssh package with a higher-level API for building
43
SSH servers. The goal of the API was to make it as simple as using net/http, so
54
the API is very similar.
@@ -42,6 +41,5 @@ exposed to you via the Session interface.
4241
4342
The one big feature missing from the Session abstraction is signals. This was
4443
started, but not completed. Pull Requests welcome!
45-
4644
*/
4745
package ssh

server.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ type Server struct {
2424
HostSigners []Signer // private keys for the host key, must have at least one
2525
Version string // server version to be sent before the initial handshake
2626

27-
PasswordHandler PasswordHandler // password authentication handler
28-
PublicKeyHandler PublicKeyHandler // public key authentication handler
29-
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
30-
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
31-
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
27+
PasswordHandler PasswordHandler // password authentication handler
28+
PublicKeyHandler PublicKeyHandler // public key authentication handler
29+
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
30+
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
31+
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
32+
ReversePortForwardingCallback ReversePortForwardingCallback //callback for allowing reverse port forwarding, denies all if nil
3233

3334
IdleTimeout time.Duration // connection timeout when no activity, none if empty
3435
MaxTimeout time.Duration // absolute connection timeout, none if empty
3536

3637
channelHandlers map[string]channelHandler
38+
requestHandlers map[string]RequestHandler
3739

3840
listenerWg sync.WaitGroup
3941
mu sync.Mutex
@@ -42,6 +44,9 @@ type Server struct {
4244
connWg sync.WaitGroup
4345
doneChan chan struct{}
4446
}
47+
type RequestHandler interface {
48+
HandleRequest(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
49+
}
4550

4651
// internal for now
4752
type channelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
@@ -57,6 +62,19 @@ func (srv *Server) ensureHostSigner() error {
5762
return nil
5863
}
5964

65+
func (srv *Server) ensureHandlers() {
66+
srv.mu.Lock()
67+
defer srv.mu.Unlock()
68+
srv.requestHandlers = map[string]RequestHandler{
69+
"tcpip-forward": forwardedTCPHandler{},
70+
"cancel-tcpip-forward": forwardedTCPHandler{},
71+
}
72+
srv.channelHandlers = map[string]channelHandler{
73+
"session": sessionHandler,
74+
"direct-tcpip": directTcpipHandler,
75+
}
76+
}
77+
6078
func (srv *Server) config(ctx Context) *gossh.ServerConfig {
6179
config := &gossh.ServerConfig{}
6280
for _, signer := range srv.HostSigners {
@@ -144,6 +162,7 @@ func (srv *Server) Shutdown(ctx context.Context) error {
144162
//
145163
// Serve always returns a non-nil error.
146164
func (srv *Server) Serve(l net.Listener) error {
165+
srv.ensureHandlers()
147166
defer l.Close()
148167
if err := srv.ensureHostSigner(); err != nil {
149168
return err
@@ -217,7 +236,8 @@ func (srv *Server) handleConn(newConn net.Conn) {
217236

218237
ctx.SetValue(ContextKeyConn, sshConn)
219238
applyConnMetadata(ctx, sshConn)
220-
go gossh.DiscardRequests(reqs)
239+
//go gossh.DiscardRequests(reqs)
240+
go srv.handleRequests(ctx, reqs)
221241
for ch := range chans {
222242
handler, found := srv.channelHandlers[ch.ChannelType()]
223243
if !found {
@@ -228,6 +248,22 @@ func (srv *Server) handleConn(newConn net.Conn) {
228248
}
229249
}
230250

251+
func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
252+
for req := range in {
253+
handler, found := srv.requestHandlers[req.Type]
254+
if !found && req.WantReply {
255+
req.Reply(false, nil)
256+
continue
257+
}
258+
/*reqCtx, cancel := context.WithCancel(ctx)
259+
defer cancel() */
260+
ret, payload := handler.HandleRequest(ctx, srv, req)
261+
if req.WantReply {
262+
req.Reply(ret, payload)
263+
}
264+
}
265+
}
266+
231267
// ListenAndServe listens on the TCP network address srv.Addr and then calls
232268
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
233269
// ListenAndServe always returns a non-nil error.

session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
282282
req.Reply(true, nil)
283283
default:
284284
// TODO: debug log
285+
req.Reply(false, nil)
285286
}
286287
}
287288
}

session_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
)
1212

1313
func (srv *Server) serveOnce(l net.Listener) error {
14+
srv.ensureHandlers()
1415
if err := srv.ensureHostSigner(); err != nil {
1516
return err
1617
}

ssh.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ type ConnCallback func(conn net.Conn) net.Conn
5050
// LocalPortForwardingCallback is a hook for allowing port forwarding
5151
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
5252

53+
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
54+
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
55+
5356
// Window represents the size of a PTY window.
5457
type Window struct {
5558
Width int

tcpip.go

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,41 @@ package ssh
22

33
import (
44
"io"
5+
"log"
56
"net"
67
"strconv"
8+
"sync"
79

810
gossh "golang.org/x/crypto/ssh"
911
)
1012

13+
const (
14+
forwardedTCPChannelType = "forwarded-tcpip"
15+
)
16+
1117
// direct-tcpip data struct as specified in RFC4254, Section 7.2
12-
type forwardData struct {
13-
DestinationHost string
14-
DestinationPort uint32
18+
type localForwardChannelData struct {
19+
DestAddr string
20+
DestPort uint32
1521

16-
OriginatorHost string
17-
OriginatorPort uint32
22+
OriginAddr string
23+
OriginPort uint32
1824
}
1925

2026
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
21-
d := forwardData{}
27+
d := localForwardChannelData{}
2228
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
2329
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
2430
return
2531
}
2632

27-
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
33+
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
2834
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
2935
return
3036
}
3137

32-
dest := net.JoinHostPort(d.DestinationHost, strconv.FormatInt(int64(d.DestinationPort), 10))
33-
38+
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
39+
3440
var dialer net.Dialer
3541
dconn, err := dialer.DialContext(ctx, "tcp", dest)
3642
if err != nil {
@@ -56,3 +62,127 @@ func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewCh
5662
io.Copy(dconn, ch)
5763
}()
5864
}
65+
66+
type remoteForwardRequest struct {
67+
BindAddr string
68+
BindPort uint32
69+
}
70+
71+
type remoteForwardSuccess struct {
72+
BindPort uint32
73+
}
74+
75+
type remoteForwardCancelRequest struct {
76+
BindAddr string
77+
BindPort uint32
78+
}
79+
80+
type remoteForwardChannelData struct {
81+
DestAddr string
82+
DestPort uint32
83+
OriginAddr string
84+
OriginPort uint32
85+
}
86+
87+
type forwardedTCPHandler struct {
88+
forwards map[string]net.Listener
89+
sync.Mutex
90+
}
91+
92+
func (h forwardedTCPHandler) HandleRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
93+
h.Lock()
94+
if h.forwards == nil {
95+
h.forwards = make(map[string]net.Listener)
96+
}
97+
h.Unlock()
98+
conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
99+
switch req.Type {
100+
case "tcpip-forward":
101+
var reqPayload remoteForwardRequest
102+
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
103+
// TODO: log parse failure
104+
return false, []byte{}
105+
}
106+
if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) {
107+
return false, []byte("port forwarding is disabled")
108+
}
109+
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
110+
ln, err := net.Listen("tcp", addr)
111+
if err != nil {
112+
// TODO: log listen failure
113+
return false, []byte{}
114+
}
115+
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
116+
destPort, _ := strconv.Atoi(destPortStr)
117+
h.Lock()
118+
h.forwards[addr] = ln
119+
h.Unlock()
120+
go func() {
121+
<-ctx.Done()
122+
h.Lock()
123+
ln, ok := h.forwards[addr]
124+
h.Unlock()
125+
if ok {
126+
ln.Close()
127+
}
128+
}()
129+
go func() {
130+
for {
131+
c, err := ln.Accept()
132+
if err != nil {
133+
// TODO: log accept failure
134+
break
135+
}
136+
originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String())
137+
originPort, _ := strconv.Atoi(orignPortStr)
138+
payload := gossh.Marshal(&remoteForwardChannelData{
139+
DestAddr: reqPayload.BindAddr,
140+
DestPort: uint32(destPort),
141+
OriginAddr: originAddr,
142+
OriginPort: uint32(originPort),
143+
})
144+
go func() {
145+
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
146+
if err != nil {
147+
// TODO: log failure to open channel
148+
log.Println(err)
149+
c.Close()
150+
return
151+
}
152+
go gossh.DiscardRequests(reqs)
153+
go func() {
154+
defer ch.Close()
155+
defer c.Close()
156+
io.Copy(ch, c)
157+
}()
158+
go func() {
159+
defer ch.Close()
160+
defer c.Close()
161+
io.Copy(c, ch)
162+
}()
163+
}()
164+
}
165+
h.Lock()
166+
delete(h.forwards, addr)
167+
h.Unlock()
168+
}()
169+
return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)})
170+
171+
case "cancel-tcpip-forward":
172+
var reqPayload remoteForwardCancelRequest
173+
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
174+
// TODO: log parse failure
175+
return false, []byte{}
176+
}
177+
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
178+
h.Lock()
179+
ln, ok := h.forwards[addr]
180+
h.Unlock()
181+
if ok {
182+
ln.Close()
183+
}
184+
return true, nil
185+
default:
186+
return false, nil
187+
}
188+
}

0 commit comments

Comments
 (0)