Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add registered clients feature #86

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
197 changes: 185 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,37 +79,210 @@ Sample configuration that exposes:
looks like this

```yaml
server_addr: SERVER_IP:5223
tunnels:
webui:
proto: http
addr: localhost:8080
auth: user:password
host: webui.my-tunnel-host.com
ssh:
proto: tcp
addr: 192.168.0.5:22
remote_addr: 0.0.0.0:22
server_addr: SERVER_IP:5223
tunnels:
webui:
proto: http
local_addr: localhost:8080
auth: user:password
host: webui.my-tunnel-host.com
ssh:
proto: tcp
local_addr: 192.168.0.5:22
remote_addr: 0.0.0.0:22
```

Configuration options:

* `name`: set the client name (not required).
* `description`: set the client name (not required).
* `server_addr`: server TCP address, i.e. `54.12.12.45:5223`
* `tls_crt`: path to client TLS certificate, *default:* `client.crt` *in the config file directory*
* `tls_key`: path to client TLS certificate key, *default:* `client.key` *in the config file directory*
* `root_ca`: path to trusted root certificate authority pool file, if empty any server certificate is accepted
* `tunnels / [name]`
* `proto`: tunnel protocol, `http` or `tcp`
* `addr`: forward traffic to this local port number or network address, for `proto=http` this can be full URL i.e. `https://machine/sub/path/?plus=params`, supports URL schemes `http` and `https`
* `local_addr`: forward traffic to this local port number or network address, for `proto=http` this can be full URL i.e. `https://machine/sub/path/?plus=params`, supports URL schemes `http` and `https`
* `auth`: (`proto=http`) (optional) basic authentication credentials to enforce on tunneled requests, format `user:password`
* `host`: (`proto=http`) hostname to request (requires reserved name and DNS CNAME)
* `remote_addr`: (`proto=tcp`) bind the remote TCP address
* `disabled`: if set to `true` this tunnel has be ignored.
* `backoff`
* `interval`: how long client would wait before redialing the server if connection was lost, exponential backoff initial interval, *default:* `500ms`
* `multiplier`: interval multiplier if reconnect failed, *default:* `1.5`
* `max_interval`: maximal time client would wait before redialing the server, *default:* `1m`
* `max_time`: maximal time client would try to reconnect to the server if connection was lost, set `0` to never stop trying, *default:* `15m`

## Client Registration

### Client

Configuration file `tunnel.yaml`:

```yaml
registered: true
server_addr: SERVER_IP:5223
tunnels:
webui:
proto: http
local_addr: localhost:8080
auth: user:password
ssh:
proto: tcp
local_addr: 192.168.0.5:22
```

Configuration options:

* `registered`: registered client. Set as `true`.
* `server_addr`: server TCP address, i.e. `54.12.12.45:5223`
* `tls_crt`: path to client TLS certificate, *default:* `client.crt` *in the config file directory*
* `tls_key`: path to client TLS certificate key, *default:* `client.key` *in the config file directory*
* `root_ca`: path to trusted root certificate authority pool file, if empty any server certificate is accepted
* `tunnels / [name]`
* `proto`: tunnel protocol, `http` or `tcp`
* `local_addr`: forward traffic to this local port number or network address, for `proto=http` this can be full URL i.e. `https://machine/sub/path/?plus=params`, supports URL schemes `http` and `https`
* `auth`: (`proto=http`) (optional) basic authentication credentials to enforce on tunneled requests, format `user:password`
* `disabled`: if set to `true` this tunnel has be ignored.
* `backoff`
* `interval`: how long client would wait before redialing the server if connection was lost, exponential backoff initial interval, *default:* `500ms`
* `multiplier`: interval multiplier if reconnect failed, *default:* `1.5`
* `max_interval`: maximal time client would wait before redialing the server, *default:* `1m`
* `max_time`: maximal time client would try to reconnect to the server if connection was lost, set `0` to never stop trying, *default:* `15m`

### Server

Create registered clients database structure if not exists:

```bash
$ mkdir clients
```

On client, get ID: `$ tunnel id`

Registered **Client DIR** `clients/CLIENT_ID`.

Configure client (file `config.yaml`).
Example `clients/SS2KPSV-5KG2URM-WYUEDLY-FBDAD7A-MUUVTDX-TL7KL45-2PQEQAD-IN4LVAH/config.yaml`:

```yaml
connections: 4
tunnels:
webui:
proto: http
host: mps.dea.ufv.br
ssh:
proto: tcp
remote_addr: 127.0.0.1:2222
```

Configuration options:
* `disabled`: if set to `true` block this client. Or create empty file `disabled` on client dir.
Example: `$ touch clients/CLIENT_ID/disabled`.
* `connections`: number of connections in addition to the main connection. Default is `0`.
* `tunnels / [name]`
* `proto`: tunnel protocol, `http` or `tcp`
* `host`: (`proto=http`) hostname to request (requires reserved name and DNS CNAME)
* `remote_addr`: (`proto=tcp`) bind the remote TCP address
* `disabled`: if set to `true` this tunnel has be ignored.

### Run

Server:
```bash
$ tunneld -clientsDir ./clients

# or (using default clients dir, only if dir exists)

$ tunneld
```

Client:
```bash
$ tunnel start-all
```

#### With Supervisor

Server example:

[program:tunneld]
directory=/etc/tunneld
command=/usr/bin/tunneld
autostart=true
autorestart=true
stdout_logfile=/var/log/tunneld.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=2
redirect_stderr=true
user = root
environment = HOME="/root", USER="root"

Client example:

[program:tunnel]
directory=/etc/tunnel
command=/usr/bin/tunnel
autostart=true
autorestart=true
startsecs=10
stdout_logfile=/var/log/tunnel.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=2
redirect_stderr=true
user = root
environment = HOME="/root", USER="root"

### Embeded

#### tunnel
Example for embed `tunnel` in your code.

```go
package main

import "github.com/mmatczuk/go-http-tunnel/cli/tunnel"

func tunnelClient() {
options, err := tunnel.ParseArgs(false, "mycmd", "start-all")
if err != nil {
panic(err)
}
tunnel.MainConfigOptions(&tunnel.ClientConfig{
ServerAddr: "domain.com:2000",
Tunnels: map[string]*tunnel.Tunnel{
"main": {
Protocol: "tcp",
LocalAddr: "localhost:5000",
RemoteAddr: "domain.com:5000",
},
},
}, options)
}

func main() {
go tunnelClient()

// ... my system code
}
```

#### tunneld

Example for embed `tunneld` in your code.

```go
package main

import "github.com/mmatczuk/go-http-tunnel/cli/tunneld"

func main() {
go tunneld.MainArgs("mycmd", "-log-level", "3", "-httpAddr", ":19000", "-httpsAddr", ":19001")

// ... my system code
}
```

## How it works

A client opens TLS connection to a server. The server accepts connections from known clients only. The client is recognized by its TLS certificate ID. The server is publicly available and proxies incoming connections to the client. Then the connection is further proxied in the client's network.
Expand Down
66 changes: 47 additions & 19 deletions cmd/tunnel/config.go → cli/tunnel/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// Use of this source code is governed by an AGPL-style
// license that can be found in the LICENSE file.

package main
package tunnel

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"

Expand All @@ -21,6 +22,7 @@ const (
DefaultBackoffMultiplier = 1.5
DefaultBackoffMaxInterval = 60 * time.Second
DefaultBackoffMaxTime = 15 * time.Minute
ConfigFileSTDIN = "-"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it's not part of "Default backoff configuration."

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only stdin file path reference.

)

// BackoffConfig defines behavior of staggering reconnection retries.
Expand All @@ -34,14 +36,15 @@ type BackoffConfig struct {
// Tunnel defines a tunnel.
type Tunnel struct {
Protocol string `yaml:"proto,omitempty"`
Addr string `yaml:"addr,omitempty"`
LocalAddr string `yaml:"local_addr,omitempty"`
Auth string `yaml:"auth,omitempty"`
Host string `yaml:"host,omitempty"`
RemoteAddr string `yaml:"remote_addr,omitempty"`
}

// ClientConfig is a tunnel client configuration.
type ClientConfig struct {
Registered bool `yaml:"registered"`
ServerAddr string `yaml:"server_addr"`
TLSCrt string `yaml:"tls_crt"`
TLSKey string `yaml:"tls_key"`
Expand All @@ -50,15 +53,30 @@ type ClientConfig struct {
Tunnels map[string]*Tunnel `yaml:"tunnels"`
}

func loadClientConfigFromFile(file string) (*ClientConfig, error) {
buf, err := ioutil.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("failed to read file %q: %s", file, err)
func LoadClientConfigFromFile(file string) (*ClientConfig, error) {
var (
buf []byte
err error
)

var configDir string

if file == ConfigFileSTDIN {
if buf, err = ioutil.ReadAll(os.Stdin); err != nil {
return nil, fmt.Errorf("failed to read config from STDIN: ", err)
}
file = "STDIN"
configDir = "."
} else {
configDir = filepath.Dir(file)
if buf, err = ioutil.ReadFile(file); err != nil {
return nil, fmt.Errorf("failed to read file %q: %s", file, err)
}
}

c := ClientConfig{
TLSCrt: filepath.Join(filepath.Dir(file), "client.crt"),
TLSKey: filepath.Join(filepath.Dir(file), "client.key"),
TLSCrt: "client.crt",
TLSKey: "client.key",
Backoff: BackoffConfig{
Interval: DefaultBackoffInterval,
Multiplier: DefaultBackoffMultiplier,
Expand All @@ -71,6 +89,14 @@ func loadClientConfigFromFile(file string) (*ClientConfig, error) {
return nil, fmt.Errorf("failed to parse file %q: %s", file, err)
}

if filepath.Dir(c.TLSCrt) == "." {
c.TLSCrt = filepath.Join(configDir, c.TLSCrt)
}

if filepath.Dir(c.TLSKey) == "." {
c.TLSKey = filepath.Join(configDir, c.TLSKey)
}

if c.ServerAddr == "" {
return nil, fmt.Errorf("server_addr: missing")
}
Expand All @@ -81,11 +107,11 @@ func loadClientConfigFromFile(file string) (*ClientConfig, error) {
for name, t := range c.Tunnels {
switch t.Protocol {
case proto.HTTP:
if err := validateHTTP(t); err != nil {
if err := validateHTTP(c.Registered, t); err != nil {
return nil, fmt.Errorf("%s %s", name, err)
}
case proto.TCP, proto.TCP4, proto.TCP6:
if err := validateTCP(t); err != nil {
if err := validateTCP(c.Registered, t); err != nil {
return nil, fmt.Errorf("%s %s", name, err)
}
default:
Expand All @@ -96,15 +122,15 @@ func loadClientConfigFromFile(file string) (*ClientConfig, error) {
return &c, nil
}

func validateHTTP(t *Tunnel) error {
func validateHTTP(registered bool, t *Tunnel) error {
var err error
if t.Host == "" {
if !registered && t.Host == "" {
return fmt.Errorf("host: missing")
}
if t.Addr == "" {
if t.LocalAddr == "" {
return fmt.Errorf("addr: missing")
}
if t.Addr, err = normalizeURL(t.Addr); err != nil {
if t.LocalAddr, err = normalizeURL(t.LocalAddr); err != nil {
return fmt.Errorf("addr: %s", err)
}

Expand All @@ -117,15 +143,17 @@ func validateHTTP(t *Tunnel) error {
return nil
}

func validateTCP(t *Tunnel) error {
func validateTCP(registered bool, t *Tunnel) error {
var err error
if t.RemoteAddr, err = normalizeAddress(t.RemoteAddr); err != nil {
return fmt.Errorf("remote_addr: %s", err)
if !registered {
if t.RemoteAddr, err = normalizeAddress(t.RemoteAddr); err != nil {
return fmt.Errorf("remote_addr: %s", err)
}
}
if t.Addr == "" {
if t.LocalAddr == "" {
return fmt.Errorf("addr: missing")
}
if t.Addr, err = normalizeAddress(t.Addr); err != nil {
if t.LocalAddr, err = normalizeAddress(t.LocalAddr); err != nil {
return fmt.Errorf("addr: %s", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/tunnel/normalize.go → cli/tunnel/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by an AGPL-style
// license that can be found in the LICENSE file.

package main
package tunnel

import (
"fmt"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by an AGPL-style
// license that can be found in the LICENSE file.

package main
package tunnel

import (
"strings"
Expand Down
Loading