diff --git a/cmd/telebit/telebit.go b/cmd/telebit/telebit.go index df8f7ce..a64b970 100644 --- a/cmd/telebit/telebit.go +++ b/cmd/telebit/telebit.go @@ -47,9 +47,10 @@ var ( ) type Forward struct { - scheme string - pattern string - port string + scheme string + pattern string + port string + localTLS bool } var authorizer telebit.Authorizer @@ -83,6 +84,7 @@ func main() { secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)") token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)") bindAddrsStr := flag.String("listen", "", "list of bind addresses on which to listen, such as localhost:80, or :443") + tlsLocals := flag.String("tls-locals", "", "like --locals, but TLS will be used to connect to the local port") locals := flag.String("locals", "", "a list of :") portToPorts := flag.String("port-forward", "", "a list of : for raw port-forwarding") verbose := flag.Bool("verbose", false, "log excessively") @@ -147,6 +149,32 @@ func main() { domains = append(domains, domain) } + if 0 == len(*tlsLocals) { + *tlsLocals = os.Getenv("TLS_LOCALS") + } + for _, cfg := range strings.Fields(strings.ReplaceAll(*tlsLocals, ",", " ")) { + parts := strings.Split(cfg, ":") + last := len(parts) - 1 + port := parts[last] + domain := parts[last-1] + scheme := "" + if len(parts) > 2 { + scheme = parts[0] + } + forwards = append(forwards, Forward{ + scheme: scheme, + pattern: domain, + port: port, + localTLS: true, + }) + + // don't load wildcard into jwt domains + if "*" == domain { + continue + } + domains = append(domains, domain) + } + if 0 == len(*portToPorts) { *portToPorts = os.Getenv("PORT_FORWARDS") } @@ -467,7 +495,12 @@ func muxAll( for _, fwd := range forwards { //mux.ForwardTCP("*", "localhost:"+fwd.port, 120*time.Second) if "https" == fwd.scheme { - mux.ReverseProxyHTTP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Reverse Proxy]") + if fwd.localTLS { + // this doesn't make much sense, but... security theatre + mux.ReverseProxyHTTPS(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Reverse Proxy TLS]") + } else { + mux.ReverseProxyHTTP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Reverse Proxy]") + } } mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Forward]") } diff --git a/examples/run-as-client.sh b/examples/run-as-client.sh index 01f9d06..38a0237 100644 --- a/examples/run-as-client.sh +++ b/examples/run-as-client.sh @@ -42,6 +42,7 @@ VERBOSE_RAW=${VERBOSE_RAW:-} --secret "$CLIENT_SECRET" \ --tunnel-relay-url $TUNNEL_RELAY_URL \ --listen "$LISTEN" \ + --tls-locals "$TLS_LOCALS" \ --locals "$LOCALS" \ --acme-agree=${ACME_AGREE} \ --acme-email "$ACME_EMAIL" \ diff --git a/routemux.go b/routemux.go index 2c3cbc2..2872797 100644 --- a/routemux.go +++ b/routemux.go @@ -158,6 +158,16 @@ func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Dur return nil } +func (m *RouteMux) ReverseProxyHTTPS(servername string, target string, timeout time.Duration, comment ...string) error { + m.routes = append(m.routes, meta{ + addr: servername, + terminate: false, + handler: NewTheatricalProxier(target, timeout), + comment: append(comment, "")[0], + }) + return nil +} + func (m *RouteMux) ReverseProxyHTTP(servername string, target string, timeout time.Duration, comment ...string) error { m.routes = append(m.routes, meta{ addr: servername, diff --git a/telebit.go b/telebit.go index 01f5cc0..0bdbd98 100644 --- a/telebit.go +++ b/telebit.go @@ -69,15 +69,72 @@ func NewForwarder(target string, timeout time.Duration) HandlerFunc { } } +// NewTheatricalProxier exists because... reasons... but should not be used +func NewTheatricalProxier(target string, timeout time.Duration) HandlerFunc { + return newReverseProxier(target, timeout, true) +} + func NewReverseProxier(target string, timeout time.Duration) HandlerFunc { + return newReverseProxier(target, timeout, false) +} + +func newReverseProxier(target string, timeout time.Duration, theatre bool) HandlerFunc { // TODO accept listener? proxyListener := httpshim.NewListener() - myURL, err := url.Parse("http://" + target) + scheme := "http://" + if theatre { + scheme = "https://" + } + targetURL, err := url.Parse(scheme + target) if nil != err { panic(err) } - // TODO headers - proxyHandler := httputil.NewSingleHostReverseProxy(myURL) + //proxyHandler := httputil.NewSingleHostReverseProxy(targetURL) + proxyHandler := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Port") + + targetQuery := targetURL.RawQuery + req.URL.Scheme = targetURL.Scheme + req.URL.Host = targetURL.Host + req.URL.Path, req.URL.RawPath = joinURLPath(targetURL, req.URL) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } + }, + } + if theatre { + /* + // TODO we could take control of the SNI here + proxyHandler.Transport = &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // would need timeout + dialer := tls.Dialer{ + Config: &tls.Config{ + ServerName: "localhost", + InsecureSkipVerify: true, + TLSHandshakeTimeout: 10 * time.Second, + }, + } + return dialer.DialContext(ctx, network, addr) + }, + } + //*/ + ///* + proxyHandler.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + TLSHandshakeTimeout: 10 * time.Second, + } + //*/ + } proxyServer := &http.Server{ Handler: proxyHandler, } @@ -92,6 +149,40 @@ func NewReverseProxier(target string, timeout time.Duration) HandlerFunc { } } +// Taken from https://golang.org/src/net/http/httputil/reverseproxy.go +func joinURLPath(a, b *url.URL) (path, rawpath string) { + if a.RawPath == "" && b.RawPath == "" { + return singleJoiningSlash(a.Path, b.Path), "" + } + // Same as singleJoiningSlash, but uses EscapedPath to determine + // whether a slash should be added + apath := a.EscapedPath() + bpath := b.EscapedPath() + + aslash := strings.HasSuffix(apath, "/") + bslash := strings.HasPrefix(bpath, "/") + + switch { + case aslash && bslash: + return a.Path + b.Path[1:], apath + bpath[1:] + case !aslash && !bslash: + return a.Path + "/" + b.Path, apath + "/" + bpath + } + return a.Path + b.Path, apath + bpath +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +} + // Forward port-forwards a relay (websocket) client to a target (local) server func Forward(client net.Conn, target net.Conn, timeout time.Duration) error {