diff --git a/README.md b/README.md index 33572dd..715a05c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -55,7 +55,7 @@ Run client: $ tunnel -config ./tunnel/tunnel.yml start-all ``` -Run server: +### Run server: * Install `tunneld` binary * Make `.tunneld` directory @@ -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). @@ -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. @@ -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": [ + "hannes.asacloud.eu" + ] + } +] +``` + ## 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. diff --git a/cmd/tunneld/api.go b/cmd/tunneld/api.go new file mode 100644 index 0000000..cc921dd --- /dev/null +++ b/cmd/tunneld/api.go @@ -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)) +} diff --git a/cmd/tunneld/options.go b/cmd/tunneld/options.go index af549b9..c58dd40 100644 --- a/cmd/tunneld/options.go +++ b/cmd/tunneld/options.go @@ -42,6 +42,7 @@ type options struct { httpAddr string httpsAddr string tunnelAddr string + apiAddr string sniAddr string tlsCrt string tlsKey string @@ -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") @@ -68,6 +70,7 @@ func parseArgs() *options { httpAddr: *httpAddr, httpsAddr: *httpsAddr, tunnelAddr: *tunnelAddr, + apiAddr: *apiAddr, sniAddr: *sniAddr, tlsCrt: *tlsCrt, tlsKey: *tlsKey, diff --git a/cmd/tunneld/tunneld.go b/cmd/tunneld/tunneld.go index 8d49e8b..55f294e 100644 --- a/cmd/tunneld/tunneld.go +++ b/cmd/tunneld/tunneld.go @@ -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() { diff --git a/server.go b/server.go index 15fa802..dc3ae65 100644 --- a/server.go +++ b/server.go @@ -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, @@ -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 +}