Skip to content

Commit

Permalink
rename to spf and simply code
Browse files Browse the repository at this point in the history
  • Loading branch information
easeway committed Aug 8, 2020
1 parent 8839e9c commit 313602a
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 71 deletions.
49 changes: 30 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`.
11 changes: 5 additions & 6 deletions cmd/epd/main.go → cmd/spf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)

Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
22 changes: 6 additions & 16 deletions pkg/endpoint/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,22 @@ package endpoint

import (
"context"
"fmt"
"os"
"os/exec"

"github.com/evo-cloud/epd/pkg/ssh"
)

// Exec invokes external program for setting up an endpoint.
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
Expand Down
84 changes: 55 additions & 29 deletions pkg/ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net"
"os"
"os/user"
"strconv"
"strings"
"sync"

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
Expand Down

0 comments on commit 313602a

Please sign in to comment.