Compare commits

...

10 Commits

Author SHA1 Message Date
b0000000000000t
21408e9087 garbage removed 2018-11-13 15:21:07 +06:00
eliastor
bedd02c8ff
Merge branch 'master' into remote-forwarding 2018-11-13 14:14:50 +06:00
eliastor
bb40c420c7 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.

83c378c48d

* 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
2018-11-13 01:27:33 -06:00
Jeff Lindsay
d411603a3a
Merge branch 'master' into remote-forwarding 2017-11-01 18:25:41 -05:00
Jeff Lindsay
537a1bee09 tcpip: stop listening when ssh clients disconnect
Signed-off-by: Jeff Lindsay <progrium@gmail.com>
2017-09-21 17:16:06 -05:00
Jeff Lindsay
98813d13df session: always reply to unblock clients trying something
Signed-off-by: Jeff Lindsay <progrium@gmail.com>
2017-09-21 15:26:38 -05:00
Jeff Lindsay
824322f98c context: docs typo
Signed-off-by: Jeff Lindsay <progrium@gmail.com>
2017-09-21 15:25:43 -05:00
Jeff Lindsay
c6ea04edc4 tcpip: working remote forwarding
Signed-off-by: Jeff Lindsay <progrium@gmail.com>
2017-08-30 21:52:41 -05:00
Jeff Lindsay
9f705628f8 server: fixes handler setup, changed to interface based handlers, added global request handler map 2017-08-23 19:17:50 -05:00
Jeff Lindsay
53fc9a232a context: fixed documentation to be more specific about ContextKeyConn being the key for a gossh.ServerConn
Signed-off-by: Jeff Lindsay <progrium@gmail.com>
2017-08-23 19:15:50 -05:00
8 changed files with 219 additions and 19 deletions

@ -0,0 +1,31 @@
package main
import (
"io"
"log"
"github.com/gliderlabs/ssh"
)
func main() {
log.Println("starting ssh server on port 2222...")
server := ssh.Server{
LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool {
log.Println("Accepted forward", dhost, dport)
return true
}),
Addr: ":2222",
Handler: ssh.Handler(func(s ssh.Session) {
io.WriteString(s, "Remote forwarding available...\n")
select {}
}),
ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool {
log.Println("attempt to bind", host, port, "granted")
return true
}),
}
log.Fatal(server.ListenAndServe())
}

@ -48,7 +48,7 @@ var (
ContextKeyServer = &contextKey{"ssh-server"}
// ContextKeyConn is a context key for use with Contexts in this package.
// The associated value will be of type gossh.Conn.
// The associated value will be of type gossh.ServerConn.
ContextKeyConn = &contextKey{"ssh-conn"}
// ContextKeyPublicKey is a context key for use with Contexts in this package.

2
doc.go

@ -1,5 +1,4 @@
/*
Package ssh wraps the crypto/ssh package with a higher-level API for building
SSH servers. The goal of the API was to make it as simple as using net/http, so
the API is very similar.
@ -42,6 +41,5 @@ exposed to you via the Session interface.
The one big feature missing from the Session abstraction is signals. This was
started, but not completed. Pull Requests welcome!
*/
package ssh

@ -24,16 +24,18 @@ type Server struct {
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
ReversePortForwardingCallback ReversePortForwardingCallback //callback for allowing reverse port forwarding, denies all if nil
IdleTimeout time.Duration // connection timeout when no activity, none if empty
MaxTimeout time.Duration // absolute connection timeout, none if empty
channelHandlers map[string]channelHandler
requestHandlers map[string]RequestHandler
listenerWg sync.WaitGroup
mu sync.Mutex
@ -42,6 +44,9 @@ type Server struct {
connWg sync.WaitGroup
doneChan chan struct{}
}
type RequestHandler interface {
HandleRequest(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
}
// internal for now
type channelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
@ -57,6 +62,19 @@ func (srv *Server) ensureHostSigner() error {
return nil
}
func (srv *Server) ensureHandlers() {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.requestHandlers = map[string]RequestHandler{
"tcpip-forward": forwardedTCPHandler{},
"cancel-tcpip-forward": forwardedTCPHandler{},
}
srv.channelHandlers = map[string]channelHandler{
"session": sessionHandler,
"direct-tcpip": directTcpipHandler,
}
}
func (srv *Server) config(ctx Context) *gossh.ServerConfig {
config := &gossh.ServerConfig{}
for _, signer := range srv.HostSigners {
@ -144,6 +162,7 @@ func (srv *Server) Shutdown(ctx context.Context) error {
//
// Serve always returns a non-nil error.
func (srv *Server) Serve(l net.Listener) error {
srv.ensureHandlers()
defer l.Close()
if err := srv.ensureHostSigner(); err != nil {
return err
@ -217,7 +236,8 @@ func (srv *Server) handleConn(newConn net.Conn) {
ctx.SetValue(ContextKeyConn, sshConn)
applyConnMetadata(ctx, sshConn)
go gossh.DiscardRequests(reqs)
//go gossh.DiscardRequests(reqs)
go srv.handleRequests(ctx, reqs)
for ch := range chans {
handler, found := srv.channelHandlers[ch.ChannelType()]
if !found {
@ -228,6 +248,22 @@ func (srv *Server) handleConn(newConn net.Conn) {
}
}
func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
for req := range in {
handler, found := srv.requestHandlers[req.Type]
if !found && req.WantReply {
req.Reply(false, nil)
continue
}
/*reqCtx, cancel := context.WithCancel(ctx)
defer cancel() */
ret, payload := handler.HandleRequest(ctx, srv, req)
if req.WantReply {
req.Reply(ret, payload)
}
}
}
// ListenAndServe listens on the TCP network address srv.Addr and then calls
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
// ListenAndServe always returns a non-nil error.

@ -282,6 +282,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
req.Reply(true, nil)
default:
// TODO: debug log
req.Reply(false, nil)
}
}
}

@ -11,6 +11,7 @@ import (
)
func (srv *Server) serveOnce(l net.Listener) error {
srv.ensureHandlers()
if err := srv.ensureHostSigner(); err != nil {
return err
}

3
ssh.go

@ -50,6 +50,9 @@ type ConnCallback func(conn net.Conn) net.Conn
// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
// Window represents the size of a PTY window.
type Window struct {
Width int

150
tcpip.go

@ -2,35 +2,41 @@ package ssh
import (
"io"
"log"
"net"
"strconv"
"sync"
gossh "golang.org/x/crypto/ssh"
)
// direct-tcpip data struct as specified in RFC4254, Section 7.2
type forwardData struct {
DestinationHost string
DestinationPort uint32
const (
forwardedTCPChannelType = "forwarded-tcpip"
)
OriginatorHost string
OriginatorPort uint32
// direct-tcpip data struct as specified in RFC4254, Section 7.2
type localForwardChannelData struct {
DestAddr string
DestPort uint32
OriginAddr string
OriginPort uint32
}
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
d := forwardData{}
d := localForwardChannelData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
return
}
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
return
}
dest := net.JoinHostPort(d.DestinationHost, strconv.FormatInt(int64(d.DestinationPort), 10))
dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest)
if err != nil {
@ -56,3 +62,127 @@ func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewCh
io.Copy(dconn, ch)
}()
}
type remoteForwardRequest struct {
BindAddr string
BindPort uint32
}
type remoteForwardSuccess struct {
BindPort uint32
}
type remoteForwardCancelRequest struct {
BindAddr string
BindPort uint32
}
type remoteForwardChannelData struct {
DestAddr string
DestPort uint32
OriginAddr string
OriginPort uint32
}
type forwardedTCPHandler struct {
forwards map[string]net.Listener
sync.Mutex
}
func (h forwardedTCPHandler) HandleRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
h.Lock()
if h.forwards == nil {
h.forwards = make(map[string]net.Listener)
}
h.Unlock()
conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
switch req.Type {
case "tcpip-forward":
var reqPayload remoteForwardRequest
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
// TODO: log parse failure
return false, []byte{}
}
if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) {
return false, []byte("port forwarding is disabled")
}
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
ln, err := net.Listen("tcp", addr)
if err != nil {
// TODO: log listen failure
return false, []byte{}
}
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
destPort, _ := strconv.Atoi(destPortStr)
h.Lock()
h.forwards[addr] = ln
h.Unlock()
go func() {
<-ctx.Done()
h.Lock()
ln, ok := h.forwards[addr]
h.Unlock()
if ok {
ln.Close()
}
}()
go func() {
for {
c, err := ln.Accept()
if err != nil {
// TODO: log accept failure
break
}
originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String())
originPort, _ := strconv.Atoi(orignPortStr)
payload := gossh.Marshal(&remoteForwardChannelData{
DestAddr: reqPayload.BindAddr,
DestPort: uint32(destPort),
OriginAddr: originAddr,
OriginPort: uint32(originPort),
})
go func() {
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
if err != nil {
// TODO: log failure to open channel
log.Println(err)
c.Close()
return
}
go gossh.DiscardRequests(reqs)
go func() {
defer ch.Close()
defer c.Close()
io.Copy(ch, c)
}()
go func() {
defer ch.Close()
defer c.Close()
io.Copy(c, ch)
}()
}()
}
h.Lock()
delete(h.forwards, addr)
h.Unlock()
}()
return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)})
case "cancel-tcpip-forward":
var reqPayload remoteForwardCancelRequest
if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
// TODO: log parse failure
return false, []byte{}
}
addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
h.Lock()
ln, ok := h.forwards[addr]
h.Unlock()
if ok {
ln.Close()
}
return true, nil
default:
return false, nil
}
}