From 313602a28499f2d7e408f44ae5e0cdec5b9c9d46 Mon Sep 17 00:00:00 2001 From: EaseWay Hu Date: Fri, 7 Aug 2020 21:58:23 -0700 Subject: [PATCH] rename to spf and simply code --- README.md | 49 ++++++++++++++--------- cmd/{epd => spf}/main.go | 11 +++--- go.mod | 3 +- go.sum | 2 + pkg/endpoint/exec.go | 22 +++-------- pkg/ssh/server.go | 84 ++++++++++++++++++++++++++-------------- 6 files changed, 100 insertions(+), 71 deletions(-) rename cmd/{epd => spf}/main.go (77%) diff --git a/README.md b/README.md index ec8a01d..6857af4 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,52 @@ -# Edge Proxy Daemon +# Secure Port Forwarder -This is a TCP reverse proxy for edge services behind firewalls. +This is a remote port forwarder based on SSH protocol (`ssh -R`). +It only supports remote port forwarding, but not the rest of SSH features (no session/channel is supported), +so it's safer than running a full featured SSH server on a public network. -## How It Works +With Secure Port Forwarder running on a public network, +a server behind a firewall can be exposed to the public network using: -The Edge Proxy Daemon must run somewhere it's able to open a TCP port on the network -that services are being exposed to (e.g. Internet). The edge services connect to -this proxy via SSH remote port forwarding. Here's an example: +```shell +ssh -N -R www.example.com:80:localhost:8080 user@spf.example.com +``` -- Run Edge Proxy Daemon on Internet, with DNS name epd.example.com; -- A web server is running behind firewall, and it's listening on `localhost:8080`; -- On the same machine as the web server is running, run `ssh -N -R www.example.com:80:localhost:8080 user@epd.example.com` +Here assuming Secure Port Forwarder is running on `spf.example.com` on regular SSH port `22`, +and the server behind the firewall is running on `localhost:8080`. +The `ssh` command asks Secure Port Forwarder to forward `www.example.com:80` and rely it to `localhost:8080`. -Now open the browser to access `http://www.example.com`, it should reach the web server running behind the firewall. -The Edge Proxy Daemon doesn't expose the exact port requested by the SSH client, -instead, it opens a random port on localhost, and relies on a endpoint setup script -to configure another reverse proxy for forwarding the connection on the requested DNS to this local port. +However Secure Port Forwarder doesn't exposing the specified DNS and port to the public network. +Instead, it only opens a random TCP port on `localhost` and forwards connections to the SSH client. +User must provide an endpoint setup script for setting up `www.example.com:80` on some proxy server +(e.g. [traefik](https://github.com/containous/traefik)). ## Usage -Launch `epd` without arguments to use default configurations: +Launch `spf` without arguments to use default configurations: - `-addr=:2022`: listen on `:2022` as SSH server address; - Use host keys from `/etc/ssh`; - Use `~/.ssh/authorized_keys` for authorized keys; - `-bind-addr=localhost`: open random TCP port as requested on `localhost`; -In addition to that, specifying `-endpoint-exec=PROGRAM` to use `PROGRAM` for setting up a DNS based reverse proxy. +In addition to that, specifying `-setup-cmd=PROGRAM` to use `PROGRAM` for setting up a DNS based reverse proxy. For example, when using [traefik](https://github.com/containous/traefik), a shell script can be used to configure it for forwarding the request on a specific DNS to a localhost port. The `PROGRAM` is invoked as: ``` -PROGRAM open|close hostname local-port +PROGRAM open|close public-host:public-port local-host:local-port +``` + +- `open` is used to ask the script to start forwarding from `public-host:public-port` to `local-host:local-port`; +- `close` is used to ask the script to stop forwarding from `public-host:public-port`. + +According to `-bind-address=A.B.C.D` when launching `spf`, and the SSH client command line, e.g. + +```shell +ssh -N -R www.example.com:80:localhost:8080 user@spf.example.com ``` -When `local-port` is opened for `hostname` (request on the client side as `ssh -R hostname:anyport:host:port`), -`open` is used. -When the forwarding request is canceled, `close` is used. +- `public-host:public-port` is `www.example.com:80`; +- `local-host:local-port` is `A.B.C.D:port` where the `port` is a random port opened by `spf`. diff --git a/cmd/epd/main.go b/cmd/spf/main.go similarity index 77% rename from cmd/epd/main.go rename to cmd/spf/main.go index b9765f7..65812b5 100644 --- a/cmd/epd/main.go +++ b/cmd/spf/main.go @@ -8,15 +8,15 @@ import ( "strings" "syscall" - "github.com/evo-cloud/epd/pkg/endpoint" - "github.com/evo-cloud/epd/pkg/ssh" + "github.com/evo-cloud/spf/pkg/endpoint" + "github.com/evo-cloud/spf/pkg/ssh" "github.com/golang/glog" ) var ( listenAddr = flag.String("addr", ":2022", "Listening address") bindAddr = flag.String("bind-addr", "localhost", "Bind address for remote forwarding ports") - endpointExec = flag.String("endpoint-exec", "", "Endpoint setup executable") + setupCmd = flag.String("setup-cmd", "", "Endpoint setup executable") hostKeyFiles = flag.String("host-key-files", "", "Comma-separated host key files") ) @@ -38,9 +38,8 @@ func main() { } server.BindAddress = *bindAddr - if *endpointExec != "" { - configurator := &endpoint.Exec{Program: *endpointExec} - server.ListenCallback = configurator.ListenCallback(server.BindAddress) + if *setupCmd != "" { + server.Setup = &endpoint.Exec{Program: *setupCmd} } sigCh := make(chan os.Signal, 1) diff --git a/go.mod b/go.mod index dcacbeb..1a4a885 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module github.com/evo-cloud/epd +module github.com/evo-cloud/spf go 1.14 require ( + github.com/evo-cloud/epd v0.0.0-20200807060954-8839e9c13e10 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de ) diff --git a/go.sum b/go.sum index 9e0c93f..92059e7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/evo-cloud/epd v0.0.0-20200807060954-8839e9c13e10 h1:Tak2uPpzK/H5LFGjTsSc+GXHFsIsljFv5MUCJZ0LKco= +github.com/evo-cloud/epd v0.0.0-20200807060954-8839e9c13e10/go.mod h1:GnkJE7gjCMkqMedwmq6erWq+eo9/BQSzQ/kXpRDUn2A= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/pkg/endpoint/exec.go b/pkg/endpoint/exec.go index 0fe7aca..afefbe7 100644 --- a/pkg/endpoint/exec.go +++ b/pkg/endpoint/exec.go @@ -2,11 +2,8 @@ package endpoint import ( "context" - "fmt" "os" "os/exec" - - "github.com/evo-cloud/epd/pkg/ssh" ) // Exec invokes external program for setting up an endpoint. @@ -14,20 +11,13 @@ type Exec struct { Program string } -// ListenCallback returns a ssh.ListenCallback to invoke the external program. -func (x *Exec) ListenCallback(bindAddr string) ssh.ListenCallbackFunc { - return func(ctx context.Context, host string, port int, on bool) error { - action := "open" - if !on { - action = "close" - } - backend := bindAddr + fmt.Sprintf(":%d", port) - return x.invoke(ctx, action, host, backend) +// SetupForwarder implements ForwardingSetup. +func (x *Exec) SetupForwarder(ctx context.Context, remoteAddr, localAddr string, on bool) error { + action := "open" + if !on { + action = "close" } -} - -func (x *Exec) invoke(ctx context.Context, action, name, addr string) error { - cmd := exec.CommandContext(ctx, x.Program, action, name, addr) + cmd := exec.CommandContext(ctx, x.Program, action, remoteAddr, localAddr) cmd.Env = os.Environ() cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index 7be22d3..ae10b3f 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -10,6 +10,7 @@ import ( "net" "os" "os/user" + "strconv" "strings" "sync" @@ -27,6 +28,7 @@ var ( errUnsupported = errors.New("unsupported") errNotFound = errors.New("not found") errUnauthorized = errors.New("unauthorized") + errAddrInUse = errors.New("address in-use") // ErrNoHostKeys indicates no host keys are found. // It's returned by Server.DefaultConfig. @@ -116,14 +118,24 @@ func LoadAuthorizedKeys(fn string) ([]ssh.PublicKey, error) { return keys, nil } -// ListenCallbackFunc receives callback when a listener is opened or closed. -type ListenCallbackFunc func(ctx context.Context, host string, port int, on bool) error +// ForwardingSetup is optional extension to perform extra work for setting up forwarding. +type ForwardingSetup interface { + SetupForwarder(ctx context.Context, remoteAddr, localAddr string, on bool) error +} + +// ForwardingSetupFunc is func form of ForwardingSetup. +type ForwardingSetupFunc func(ctx context.Context, remoteAddr, localAddr string, on bool) error + +// SetupForwarder implements ForwardingSetup. +func (f ForwardingSetupFunc) SetupForwarder(ctx context.Context, remoteAddr, localAddr string, on bool) error { + return f(ctx, remoteAddr, localAddr, on) +} // Server implements the gateway using SSH. type Server struct { - Config ssh.ServerConfig - BindAddress string - ListenCallback ListenCallbackFunc + Config ssh.ServerConfig + BindAddress string + Setup ForwardingSetup } // NewServer creates Server. @@ -217,9 +229,9 @@ func (s *Server) serveConn(ctx context.Context, conn net.Conn) { serverConn.run(ctx) } -func (s *Server) listenCallback(ctx context.Context, host string, port int, on bool) error { - if callback := s.ListenCallback; callback != nil { - return callback(ctx, host, port, on) +func (s *Server) setupForwarder(ctx context.Context, remoteAddr, localAddr string, on bool) error { + if setup := s.Setup; setup != nil { + return setup.SetupForwarder(ctx, remoteAddr, localAddr, on) } return nil } @@ -229,6 +241,10 @@ type forwardAddr struct { BindPort uint32 } +func (a forwardAddr) String() string { + return a.BindAddr + ":" + strconv.FormatUint(uint64(a.BindPort), 10) +} + type connection struct { server *Server conn *ssh.ServerConn @@ -247,6 +263,10 @@ func (c *connection) log(format string, args ...interface{}) { } } +func (c *connection) localAddr(ln net.Listener) string { + return c.server.BindAddress + ":" + strconv.FormatUint(uint64(ln.Addr().(*net.TCPAddr).Port), 10) +} + func (c *connection) cleanup() { c.listenersLock.Lock() listeners := c.listeners @@ -301,14 +321,17 @@ func (c *connection) forwardStart(ctx context.Context, req *ssh.Request) ([]byte if err != nil { return nil, err } - c.log("REQ %s %s:%v bind-to %s", req.Type, faddr.BindAddr, faddr.BindPort, ln.Addr()) - if err := c.server.listenCallback(ctx, faddr.BindAddr, ln.Addr().(*net.TCPAddr).Port, true); err != nil { + + if !c.addListener(faddr, ln) { ln.Close() - return nil, fmt.Errorf("callback error: %w", err) + return nil, errAddrInUse } - if existing := c.addListener(ctx, faddr, ln); existing != nil { - existing.Close() + c.log("REQ %s %s bind-to %s", req.Type, faddr, ln.Addr()) + if err := c.server.setupForwarder(ctx, faddr.String(), c.localAddr(ln), true); err != nil { + ln.Close() + c.removeListener(faddr, ln) + return nil, fmt.Errorf("setup error: %w", err) } go c.forwardRun(ctx, faddr, ln) @@ -319,12 +342,13 @@ func (c *connection) forwardStart(ctx context.Context, req *ssh.Request) ([]byte } func (c *connection) forwardRun(ctx context.Context, faddr forwardAddr, ln net.Listener) { - logPrefix := fmt.Sprintf("FWD-CLOSE %s:%v bind-to %s ", faddr.BindAddr, faddr.BindPort, ln.Addr()) - localPort := ln.Addr().(*net.TCPAddr).Port + logPrefix := fmt.Sprintf("FWD-CLOSE %s bind-to %s ", faddr, ln.Addr()) + localAddr := c.localAddr(ln) go closeWhenDone(ctx, ln) defer func() { - if err := c.server.listenCallback(ctx, faddr.BindAddr, localPort, false); err != nil { - c.log("%s callback error: %v", logPrefix, err) + c.removeListener(faddr, ln) + if err := c.server.setupForwarder(ctx, faddr.String(), localAddr, false); err != nil { + c.log("%s teardown error: %v", logPrefix, err) } c.log("%s", logPrefix) ln.Close() @@ -353,11 +377,11 @@ func (c *connection) forwardConn(ctx context.Context, conn net.Conn, faddr forwa OriginPort: uint32(originAddr.Port), })) if err != nil { - c.log("FWD %s:%v from %s error: %v", faddr.BindAddr, faddr.BindPort, conn.RemoteAddr(), err) + c.log("FWD %s from %s error: %v", faddr, conn.RemoteAddr(), err) return } - c.log("FWD %s:%v from %s START", faddr.BindAddr, faddr.BindPort, conn.RemoteAddr()) - defer c.log("FWD %s:%v from %s END", faddr.BindAddr, faddr.BindPort, conn.RemoteAddr()) + c.log("FWD %s from %s START", faddr, conn.RemoteAddr()) + defer c.log("FWD %s from %s END", faddr, conn.RemoteAddr()) go ssh.DiscardRequests(reqsCh) forward(ctx, chn, conn) } @@ -367,32 +391,34 @@ func (c *connection) forwardCancel(ctx context.Context, req *ssh.Request) ([]byt if err := ssh.Unmarshal(req.Payload, &faddr); err != nil { return nil, err } - c.log("REQ %s %s:%v", req.Type, faddr.BindAddr, faddr.BindPort) - if ln := c.removeListener(ctx, faddr); ln != nil { + c.log("REQ %s %s", req.Type, faddr) + if ln := c.removeListener(faddr, nil); ln != nil { ln.Close() return nil, nil } return nil, errNotFound } -func (c *connection) addListener(ctx context.Context, faddr forwardAddr, ln net.Listener) net.Listener { +func (c *connection) addListener(faddr forwardAddr, ln net.Listener) bool { c.listenersLock.Lock() defer c.listenersLock.Unlock() if c.listeners == nil { c.listeners = make(map[forwardAddr]net.Listener) } - existing := c.listeners[faddr] + if _, ok := c.listeners[faddr]; ok { + return false + } c.listeners[faddr] = ln - return existing + return true } -func (c *connection) removeListener(ctx context.Context, faddr forwardAddr) net.Listener { +func (c *connection) removeListener(faddr forwardAddr, ln net.Listener) net.Listener { c.listenersLock.Lock() defer c.listenersLock.Unlock() - ln, ok := c.listeners[faddr] - if ok { + existing, ok := c.listeners[faddr] + if ok && (ln == nil || ln == existing) { delete(c.listeners, faddr) - return ln + return existing } return nil }