Skip to content

Commit

Permalink
Adding support for SNI/Host proxy loadbalancing
Browse files Browse the repository at this point in the history
Adding stdinproxy option (to support ssh host loadbalancing on Windows)
Removing statically assigned ports from the dyn pool (issue #1)
  • Loading branch information
dariopb committed Feb 14, 2021
1 parent e820ac5 commit ba9d8cb
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 142 deletions.
15 changes: 14 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/goreverselb/server.go",
"program": "${workspaceFolder}/cmd/goreverselb",
"env": {
"LOGLEVEL" : "debug",
"TOKEN" : "1234",
Expand Down Expand Up @@ -37,6 +37,19 @@
},
"args": [ "tunnel" ]
},
{
"name": "Debug proxy",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/goreverselb",
"env": {
"LOGLEVEL" : "debug",
"TOKEN" : "1234",
"SERVICE_ENDPOINT" : "www.google.com:80",
},
"args": [ "stdinproxy" ]
},
{
"name": "Launch",
"type": "go",
Expand Down
99 changes: 74 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,36 @@
```


reverselb is a L4 reverse tunnel and load balancer: it creates an encrypted TLS tunnel to an external ingress that in turns receives requests on the specified port and forwards the traffic to the client via multiplexed sessions. Since it operates at L4 (tcp only for now), it allows to tunnel almost every protocol that runs on top of TCP (plain TCP, HTTP, HTTPS, WS, MQTT, etc). It is inspired on services such as [ngrok](http://ngrok.com) and [inlets](https://github.com/inlets/inlets): those are great services but they either have limits or don't support TCP tunnels (and k8s LoadBalance for them) in their free tiers.
reverselb is a L4 reverse tunnel and load balancer: it creates an encrypted TLS tunnel to an external ingress that in turns receives requests on the specified port and forwards the traffic to the client via encrypted, multiplexed sessions. Since it operates at L4 layer (tcp only for now), it allows to tunnel almost every protocol that runs on top of TCP (plain TCP, HTTP, HTTPS, WS, MQTT, SSH etc). It was inspired on services such as [ngrok](http://ngrok.com) and [inlets](https://github.com/inlets/inlets): those are great services but they either have limits or don't support TCP tunnels (and k8s LoadBalance for them) in their free tiers.

There is a client and a server components. The server is intended to be run on a machine/container that has a publicly accessible endpoint (there is an Azure ACI sample template for quick deployment) while the client runs on the private network and configures the service that needs to be accesible externally.
The client (either via the cmd line application or library) makes an outbound connection to the reverselb server and configures the tunnel properties. The server now starts listening on the port the client instructed it to and when connections are received on this port, it relays the data back and forth between it and the backend connection. As many connections as desired can be established.
There is a client and a server components. The server is intended to be run on a machine/container that has a publicly accessible endpoint (there is an Azure ACI sample template below for quick deployment) while the client runs on the private network and configures the service that needs to be accesible externally.

The client (either via the cmd line application, library or container orchestrator extension) makes an TLS protected outbound connection to the reverselb server and configures the tunnel endpoints properties. The server now starts listening externally on the port the client instructed it to and when connections are received on this port, it relays the data back and forth between it and the backend connection. As many connections as desired can be established.


## SNI/Host proxy loadbalancing

The reverselb server will try to do protocol identification in order to get a possible SNI/Hostname style redirection on the same tunnel port (to be able to share the same service port with multiple service instances). The load balancing is done on serive instance names if multiple registrations for the same name/port are made.

The currently supported protos are:
* HTTP **_connect_** protocol
* Custom HA-PROXY like protocol (**_PROXY->_[byte_len]_instanceName_**)
* HTTP **_Host_** header _[not yet added]_
* TLS ClientHello **_SNI_** extension

For example, to proxy multiple ssh servers on port 8001:

Using regular *connect* command:

```
ssh dario@instancename -o "ProxyCommand=connect -H localhost:8001 instancename 888"
```

Using embedded proxy support (executable option _stdinproxy_):

```
ssh dario@instancename -o "ProxyCommand=./goreverselb -l debug -t 0 stdinproxy -e localhost:8001"
```

# Console application

Expand All @@ -23,38 +49,37 @@ USAGE:
goreverselb [global options] command [command options] [arguments...]
COMMANDS:
server runs as a server
tunnel creates an ingress tunnel
help, h Shows a list of commands or help for one command
server runs as a server
tunnel creates an ingress tunnel
stdinproxy creates an stdin/stdout proxy to the endpoint
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--loglevel value, -l value debug level, one of: info, debug (default: "info") [$LOGLEVEL]
--token value, -t value shared secret for authorization (default: "info") [$TOKEN]
--token value, -t value shared secret for authorization [$TOKEN]
--help, -h show help (default: false)
```

# Server
## Server

```
NAME:
goreverselb [global options] server - runs as a server
goreverselb server - runs as a server
USAGE:
goreverselb server [command options] [arguments...]
OPTIONS:
--port value, -p value port for the API endpoint (default: 9999) [$PORT]
--autocertsubjectname value, -s value subject name for the autogenerated certificate (default: "localhost") [$AUTO_CERT_SUBJECT_NAME]
--port value, -p value port for the API endpoint (default: 0) [$PORT]
--autocertsubjectname value, -s value subject name for the autogenerated certificate [$AUTO_CERT_SUBJECT_NAME]
--httpport value port for the HTTP rest endpoint (server will be disabled if not provided) (default: 0) [$HTTP_PORT]
--natsport value port for the secure NATS endpoint (server will be disabled if not provided) (default: 0) [$NATS_PORT]
--dynport value dynamic frontend port base (default: 8000) [$DYN_FRONTEND_PORT]
--dynportcount value number of dynamic frontend ports (default: 100) [$DYN_FRONTEND_PORT_COUNT]
--help, -h show help (default: false)
```

# Client
## Client

```
NAME:
Expand All @@ -65,17 +90,30 @@ USAGE:
OPTIONS:
--apiendpoint value, -e value API endpoint in the form: hostname:port [$LB_API_ENDPOINT]
--frontendport value, -p value frontend port where the service is going to be exposed (endpoint will be apiendpoint:serviceport (default: 8888) [$PORT]
--serviceendpoint value, -b value backend service endpoint (the local target for the lb: hostname:port) [$SERVICE_ENDPOINT]
--servicename value, -s value service name string (default: "xxxx") [$SERVICE_NAME]
--frontendport value, -p value frontend port where the service is going to be exposed (endpoint will be apiendpoint:serviceport) (default: auto) [$PORT]
--serviceendpoint value, -b value backend service address (the local target for the lb: hostname:port) [$SERVICE_ENDPOINT]
--servicename value, -s value service name string [$SERVICE_NAME]
--instancename value instance name string (for SNI/Host functionality) (default: empty) [$INSTANCE_NAME]
--insecuretls, -i allow skip checking server CA/hostname (default: false) [$INSECURE_TLS]
--help, -h show help (default: false)
```

## Tunnel via SNI/Host
```
NAME:
goreverselb stdinproxy - creates an stdin/stdout proxy to the endpoint
USAGE:
goreverselb stdinproxy [command options] [arguments...]
OPTIONS:
--serviceendpoint value, -e value backend service address (hostname:port) [$SERVICE_ENDPOINT]
--help, -h show help (default: false)
```

# Run it!

Linux
### Linux

````
cd cmd/goreverselb
Expand All @@ -100,7 +138,20 @@ curl --header "Host: ip.jsontest.com" http://localhost:8888
[notice that we need to override the host header since the tunnel is a plain TCP one so hitting servers that rely on the host header to find the target won't work without doing so]
````

Docker (either linux, mac if you really want to try this way)
### Raspberry PI

```
cd cmd/goreverselb
export GOPATH=/home/dario/go
go get github.com/rakyll/statik
go generate ./pkg/restapi
GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 go build -v ./cmd/goreverselb
(running same a Linux)
```

## Docker (either linux, mac if you really want to try this way)

````
# server
Expand All @@ -124,15 +175,14 @@ INFO[2020-01-12T18:21:40Z] NewTunnelClient to apiEndpoint [localhost:9999] with
````

Raspberry PI (comming...)

# Kubernetes LoadBalancer Operator

You can expose you kubernetes pods externally via a LoadBalancer operator available here:


# Docker

Create docker image:
### Create docker image Linux:

````
export GOPATH=/home/dario/go
Expand All @@ -149,14 +199,14 @@ sudo docker push dariob/reverselb-alpine:0.1
````


# Raspberry PI
### Raspberry PI

````
export GOPATH=/home/dario/go
go get github.com/rakyll/statik
go generate ./pkg/restapi
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -v ./cmd/goreverselb
GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 go build -v ./cmd/goreverselb
sudo docker build -f docker/Dockerfile-pi.txt -t dariob/reverselb-pi .
sudo docker tag dariob/reverselb-pi dariob/reverselb-pi:0.1
Expand Down Expand Up @@ -361,4 +411,3 @@ The client can be embedded in your app if the backend mappings are dynamic like
tc.Close()

```

4 changes: 4 additions & 0 deletions cmd/goreverselb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func client(ctx *cli.Context) error {
log.Fatalf("wrong format for endpoint: ", err)
}

if len(instancename) > 0 {
servicename = servicename + ":" + instancename
}

td := tunnel.TunnelData{
ServiceName: servicename,
Token: token,
Expand Down
41 changes: 34 additions & 7 deletions cmd/goreverselb/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var lbapiendpoint string
var serviceendpoint string
var insecuretls bool
var servicename string
var instancename string
var token string
var frontendport int
var dynport, dynportcount int
Expand Down Expand Up @@ -66,7 +67,6 @@ func main() {
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Value: "info",
Usage: "shared secret for authorization",
EnvVars: []string{"TOKEN"},
Destination: &token,
Expand All @@ -84,7 +84,7 @@ func main() {
&cli.IntFlag{
Name: "port",
Aliases: []string{"p"},
Value: 9999,
Value: 0,
Usage: "port for the API endpoint",
EnvVars: []string{"PORT"},
Destination: &port,
Expand All @@ -94,7 +94,7 @@ func main() {
&cli.StringFlag{
Name: "autocertsubjectname",
Aliases: []string{"s"},
Value: "localhost",
Value: "",
Usage: "subject name for the autogenerated certificate",
EnvVars: []string{"AUTO_CERT_SUBJECT_NAME"},
Destination: &autocertsubjectname,
Expand Down Expand Up @@ -153,11 +153,12 @@ func main() {
&cli.IntFlag{
Name: "frontendport",
Aliases: []string{"p"},
Value: 8888,
Usage: "frontend port where the service is going to be exposed (endpoint will be apiendpoint:serviceport",
Value: 0,
DefaultText: "auto",
Usage: "frontend port where the service is going to be exposed (endpoint will be apiendpoint:serviceport)",
EnvVars: []string{"PORT"},
Destination: &frontendport,
Required: true,
Required: false,
},

&cli.StringFlag{
Expand All @@ -172,19 +173,45 @@ func main() {
&cli.StringFlag{
Name: "servicename",
Aliases: []string{"s"},
Value: "xxxx",
Usage: "service name string",
EnvVars: []string{"SERVICE_NAME"},
Destination: &servicename,
Required: true,
},
&cli.StringFlag{
Name: "instancename",
Value: "",
DefaultText: "empty",
Usage: "instance name string (for SNI/Host functionality)",
EnvVars: []string{"INSTANCE_NAME"},
Destination: &instancename,
Required: false,
},
&cli.BoolFlag{
Name: "insecuretls",
Aliases: []string{"i"},
Value: false,
Usage: "allow skip checking server CA/hostname",
EnvVars: []string{"INSECURE_TLS"},
Destination: &insecuretls,
Required: false,
},
},
},
{
Name: "stdinproxy",
//Aliases: []string{"tunnel"},
Usage: "creates an stdin/stdout proxy to the endpoint",
Action: stdinProxy,

Flags: []cli.Flag{
&cli.StringFlag{
Name: "serviceendpoint",
Aliases: []string{"e"},
Value: "",
Usage: "backend service address (hostname:port)",
EnvVars: []string{"SERVICE_ENDPOINT"},
Destination: &serviceendpoint,
Required: true,
},
},
Expand Down
54 changes: 54 additions & 0 deletions cmd/goreverselb/stdinProxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"io"
"net"
"os"
"time"

"github.com/urfave/cli/v2"

log "github.com/sirupsen/logrus"
)

func stdinProxy(ctx *cli.Context) error {
loglevel := log.DebugLevel
if l, err := log.ParseLevel(loglevelstr); err == nil {
loglevel = l
}

log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
log.SetLevel(loglevel)
log.SetOutput(os.Stdout)

doProxy()

//c := make(chan os.Signal, 2)
//signal.Notify(c, os.Interrupt, syscall.SIGTERM)

//<-c

return nil
}

func doProxy() error {
tcpAddr, err := net.ResolveTCPAddr("tcp", serviceendpoint)
if err != nil {
log.Errorf("session: [%s], failed to resolve endpoint [%s]", tcpAddr.String(), err.Error())
return err
}

d := net.Dialer{Timeout: 5 * time.Second}
backConn, err := d.Dial("tcp", tcpAddr.String())
if err != nil {
log.Errorf("session: [%s], failed to connect to [%s]", tcpAddr.String(), err.Error())
return err
}

go io.Copy(backConn, os.Stdin)
io.Copy(os.Stdout, backConn)

return nil
}
Loading

0 comments on commit ba9d8cb

Please sign in to comment.