Skip to content

Commit

Permalink
Add minimal API to query the state of the tunnel server
Browse files Browse the repository at this point in the history
  • Loading branch information
hons82 committed May 25, 2021
1 parent cf22ff5 commit 2bf0d58
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 6 deletions.
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ $ openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout client.key -out clie
$ openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout server.key -out server.crt
```

Run client:
### Run client:

* Install `tunnel` binary
* Make `.tunnel` directory in your project directory
Expand All @@ -55,7 +55,7 @@ Run client:
$ tunnel -config ./tunnel/tunnel.yml start-all
```

Run server:
### Run server:

* Install `tunneld` binary
* Make `.tunneld` directory
Expand All @@ -68,8 +68,6 @@ $ tunneld -tlsCrt .tunneld/server.crt -tlsKey .tunneld/server.key

This will run HTTP server on port `80` and HTTPS (HTTP/2) server on port `443`. If you want to use HTTPS it's recommended to get a properly signed certificate to avoid security warnings.

If both http and https are configured, an automatic redirect to the secure channel will be established using an `http.StatusMovedPermanently` (301)

### Run Server as a Service on Ubuntu using Systemd:

* After completing the steps above successfully, create a new file for your service (you can name it whatever you want, just replace the name below with your chosen name).
Expand Down Expand Up @@ -129,7 +127,7 @@ $ sudo systemctl enable tunneld.service

There are many more options for systemd services, and this is by not means an exhaustive configuration file.

## Configuration
## Configuration - Client

The tunnel client `tunnel` requires configuration file, by default it will try reading `tunnel.yml` in your current working directory. If you want to specify other file use `-config` flag.

Expand Down Expand Up @@ -176,10 +174,48 @@ Configuration options:
* `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`

## Configuration - Server

* `httpAddr`: Public address for HTTP connections, empty string to disable, *default:* `:80`
* `httpsAddr`: Public address listening for HTTPS connections, emptry string to disable, *default:* `:443`
* `tunnelAddr`: Public address listening for tunnel client, *default:* `:5223`
* `apiAddr`: Public address for HTTP API to get info about the tunnels, *default:* `:5091`
* `sniAddr`: Public address listening for TLS SNI connections, empty string to disable
* `tlsCrt`: Path to a TLS certificate file, *default:* `server.crt`
* `tlsKey`: Path to a TLS key file, *default:* `server.key`
* `rootCA`: Path to the trusted certificate chian used for client certificate authentication, if empty any client certificate is accepted
* `clients`: Comma-separated list of tunnel client ids, if empty accept all clients
* `logLevel`: Level of messages to log, 0-3, *default:* 1

If both `httpAddr` and `httpsAddr` are configured, an automatic redirect to the secure channel will be established using an `http.StatusMovedPermanently` (301)

### Custom error pages

Just copy the `html` folder from this repository into the folder of the tunnel-server to have a starting point. In the `html/errors` folder you'll find a sample page for each error that is currently customisable which you'll be able to change according to your needs.

## Status API

### /api/clients/list

Returns a list of `clients` together with a list of open tunnels in JSON format.

```json
[
{
"Id": "BHXWUUT-A6IYDWI-2BSIC5A-...",
"Listeners": [
{
"Network": "tcp",
"Addr": "192.0.2.1:25"
}
],
"Hosts": [
"tunnel1.my-tunnel-host.com"
]
}
]
```

## 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
61 changes: 61 additions & 0 deletions cmd/tunneld/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (C) 2021 Tribus Hannes
// Use of this source code is governed by an AGPL-style
// license that can be found in the LICENSE file.

package main

import (
"encoding/json"
"fmt"
"net/http"

tunnel "github.com/hons82/go-http-tunnel"
"github.com/hons82/go-http-tunnel/log"
)

// ApiConfig defines configuration for the API.
type ApiConfig struct {
// Addr is TCP address to listen for client connections. If empty ":0" is used.
Addr string
//
Server *tunnel.Server
// Logger is optional logger. If nil logging is disabled.
Logger log.Logger
}

func initAPIServer(config *ApiConfig) {

logger := config.Logger
if logger == nil {
logger = log.NewNopLogger()
}

http.HandleFunc("/api/clients/list", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
logger.Log(
"level", 2,
"action", "start client list",
)
info := config.Server.GetClientInfo()
data, err := json.Marshal(info)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
e := fmt.Sprintf("Error on unmarshall item %s", err)
w.Write([]byte(e))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(data)

logger.Log(
"level", 3,
"action", "transferred",
"bytes", len(data),
)
},
))

// Wrap our server with our gzip handler to gzip compress all responses.
fatal("can not listen on: %s", http.ListenAndServe(config.Addr, nil))
}
3 changes: 3 additions & 0 deletions cmd/tunneld/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type options struct {
httpAddr string
httpsAddr string
tunnelAddr string
apiAddr string
sniAddr string
tlsCrt string
tlsKey string
Expand All @@ -55,6 +56,7 @@ func parseArgs() *options {
httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable")
httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
tunnelAddr := flag.String("tunnelAddr", ":5223", "Public address listening for tunnel client")
apiAddr := flag.String("apiAddr", ":5091", "Public address for HTTP API to get tunnels info")
sniAddr := flag.String("sniAddr", "", "Public address listening for TLS SNI connections, empty string to disable")
tlsCrt := flag.String("tlsCrt", "server.crt", "Path to a TLS certificate file")
tlsKey := flag.String("tlsKey", "server.key", "Path to a TLS key file")
Expand All @@ -68,6 +70,7 @@ func parseArgs() *options {
httpAddr: *httpAddr,
httpsAddr: *httpsAddr,
tunnelAddr: *tunnelAddr,
apiAddr: *apiAddr,
sniAddr: *sniAddr,
tlsCrt: *tlsCrt,
tlsKey: *tlsKey,
Expand Down
16 changes: 16 additions & 0 deletions cmd/tunneld/tunneld.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ func main() {
}
}

// start API
if opts.apiAddr != "" {
go func() {
logger.Log(
"level", 1,
"action", "start api",
"addr", opts.apiAddr,
)
go initAPIServer(&ApiConfig{
Addr: opts.apiAddr,
Server: server,
Logger: logger,
})
}()
}

// start HTTP
if opts.httpAddr != "" {
go func() {
Expand Down
39 changes: 38 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func (s *Server) Start() {
}

func (s *Server) handleClient(conn net.Conn) {
logger := log.NewContext(s.logger).With("addr", conn.RemoteAddr())
logger := log.NewContext(s.logger).With("remote addr", conn.RemoteAddr())

logger.Log(
"level", 1,
Expand Down Expand Up @@ -796,3 +796,40 @@ func (s *Server) Stop() {
s.listener.Close()
}
}

type ListenerInfo struct {
Network string
Addr string
}

type ClientInfo struct {
Id string
Listeners []*ListenerInfo
Hosts []string
}

func (s *Server) GetClientInfo() []*ClientInfo {
s.registry.mu.Lock()
defer s.registry.mu.Unlock()
ret := []*ClientInfo{}
for k, v := range s.registry.items {
c := &ClientInfo{Id: k.String()}
ret = append(ret, c)
if v == voidRegistryItem {
s.logger.Log(
"level", 3,
"identifier", k.String(),
"msg", "void registry item",
)
} else {
for _, l := range v.Hosts {
c.Hosts = append(c.Hosts, l.Host)
}
for _, l := range v.Listeners {
p := &ListenerInfo{Network: l.Addr().Network(), Addr: l.Addr().String()}
c.Listeners = append(c.Listeners, p)
}
}
}
return ret
}

0 comments on commit 2bf0d58

Please sign in to comment.