From 88f704563c7d66f31e776687608d7715685f1c17 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 1 Mar 2024 10:29:45 +0800 Subject: [PATCH] README update, https redirect fix, increased timeout --- .vscode/settings.json | 0 README.md | 57 +++++++++++++++++------------ go.mod | 2 - go.sum | 4 -- main.go | 85 ++++++++++++++++++++++++++++++------------- 5 files changed, 94 insertions(+), 54 deletions(-) mode change 100644 => 100755 .vscode/settings.json mode change 100644 => 100755 README.md diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 09219a0..b84f2a7 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A simple auto docker reverse proxy for home use. -Written in **Go** with *~180 loc*. +Written in **Go** with *~220 loc*. ## Features @@ -20,11 +20,22 @@ I have tried different reverse proxy services, i.e. [nginx proxy manager](https: 2. Copy [compose.example.yml](compose.example.yml) to `compose.yml` -3. add networks to make sure it is in the same network with other containers, or make sure `proxy..host` is reachable +3. Add networks to make sure it is in the same network with other containers, or make sure `proxy..host` is reachable -4. modify the path to your SSL certs. See [Getting SSL Certs](#getting-ssl-certs) +4. Modify the path to your SSL certs. See [Getting SSL Certs](#getting-ssl-certs) -5. start `go-proxy` with `docker compose up -d`. +5. Start `go-proxy` with `docker compose up -d`. + +6. (Optional) If you are using ufw with vpn that drop all inbound traffic except vpn, run below to allow docker containers to connect to `go-proxy` + + In case the network of your container is in subnet `172.16.0.0/12` (bridge), + and vpn network is under `100.64.0.0/10` (i.e. tailscale) + + `sudo ufw allow from 172.16.0.0/12 to 100.64.0.0/10` + + You can also list CIDRs of all docker bridge networks by: + + `docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -` ## Configuration @@ -107,35 +118,35 @@ Direct connection Running 10s test @ http://homelab:4999/bench 20 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 3.71ms 2.26ms 48.10ms 94.95% - Req/Sec 1.41k 179.01 2.11k 69.97% + Latency 3.74ms 1.19ms 19.94ms 81.53% + Req/Sec 1.35k 103.96 1.60k 73.60% Latency Distribution - 50% 3.32ms - 75% 3.98ms - 90% 4.97ms - 99% 11.36ms - 282804 requests in 10.10s, 33.98MB read -Requests/sec: 27998.62 -Transfer/sec: 3.36MB + 50% 3.46ms + 75% 4.16ms + 90% 4.98ms + 99% 8.04ms + 269696 requests in 10.01s, 32.41MB read +Requests/sec: 26950.35 +Transfer/sec: 3.24MB ``` With **go-proxy** reverse proxy ```shell % wrk -t20 -c100 -d10s --latency https://whoami.mydomain.com/bench -Running 10s test @ https://whoami.mydomain.com/bench +Running 10s test @ https://whoami.6uo.me/bench 20 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev - Latency 4.41ms 2.56ms 77.80ms 95.38% - Req/Sec 1.18k 156.44 1.63k 86.51% + Latency 4.94ms 1.88ms 43.49ms 85.82% + Req/Sec 1.03k 123.57 1.22k 83.20% Latency Distribution - 50% 3.93ms - 75% 4.76ms - 90% 5.92ms - 99% 10.46ms - 235374 requests in 10.10s, 22.90MB read -Requests/sec: 23302.42 -Transfer/sec: 2.27MB + 50% 4.60ms + 75% 5.59ms + 90% 6.77ms + 99% 10.81ms + 203565 requests in 10.02s, 19.80MB read +Requests/sec: 20320.87 +Transfer/sec: 1.98MB ``` ## Build it yourself diff --git a/go.mod b/go.mod index 6c251f5..f454b9e 100755 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.21.7 require ( github.com/docker/docker v25.0.3+incompatible - github.com/jellydator/ttlcache/v3 v3.2.0 golang.org/x/text v0.14.0 ) @@ -14,7 +13,6 @@ require ( github.com/morikuni/aec v1.0.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/sdk v1.24.0 // indirect - golang.org/x/sync v0.1.0 // indirect golang.org/x/time v0.5.0 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index a0f2bf6..c65c784 100755 --- a/go.sum +++ b/go.sum @@ -31,8 +31,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= -github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= -github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -88,8 +86,6 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index f2d814e..9c9894f 100755 --- a/main.go +++ b/main.go @@ -3,12 +3,14 @@ package main import ( "fmt" "log" + "net" "net/http" "net/http/httputil" "net/url" "reflect" "runtime" "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -34,8 +36,23 @@ type Route struct { var dockerClient *client.Client var subdomainRouteMap map[string][]Route // subdomain -> path +var transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 60 * time.Second, + KeepAlive: 60 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 1000, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, +} + func NewConfig() Config { - return Config{Scheme: "http", Host: "", Port: "", Path: ""} + return Config{Scheme: "", Host: "", Port: "", Path: ""} } func main() { @@ -66,36 +83,38 @@ func main() { buildRoutes() log.Printf("[Build] built %v reverse proxies", len(subdomainRouteMap)) - http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100 + mux := http.NewServeMux() + mux.HandleFunc("/", handler) - http.HandleFunc("/", handler) go func() { log.Println("Starting HTTP server on port 80") - err := http.ListenAndServe(":80", nil) + err := http.ListenAndServe(":80", http.HandlerFunc(redirectToTLS)) if err != nil { log.Fatal("HTTP server error", err) } }() log.Println("Starting HTTPS server on port 443") - err = http.ListenAndServeTLS(":443", "/certs/cert.crt", "/certs/priv.key", nil) + err = http.ListenAndServeTLS(":443", "/certs/cert.crt", "/certs/priv.key", mux) if err != nil { log.Fatal("HTTPS Server error: ", err) } } -func redirectTLS(w http.ResponseWriter, r *http.Request) { +func redirectToTLS(w http.ResponseWriter, r *http.Request) { // Redirect to the same host but with HTTPS - http.Redirect(w, r, "https://"+r.Host+r.URL.Path, http.StatusMovedPermanently) + log.Printf("[Redirect] redirecting to https") + var redirectCode int + if r.Method == http.MethodGet { + redirectCode = http.StatusMovedPermanently + } else { + redirectCode = http.StatusPermanentRedirect + } + http.Redirect(w, r, fmt.Sprintf("https://%s%s?%s", r.Host, r.URL.Path, r.URL.RawQuery), redirectCode) } func handler(w http.ResponseWriter, r *http.Request) { - if r.TLS == nil { - redirectTLS(w, r) - return - } + log.Printf("[Request] %s %s", r.Method, r.URL.String()) subdomain := strings.Split(r.Host, ".")[0] - // log.Printf("[Request] %s%s\n", r.Host, r.URL) - routeMap, ok := subdomainRouteMap[subdomain] if !ok { http.Error(w, fmt.Sprintf("no matching route for subdomain %s", subdomain), http.StatusNotFound) @@ -103,11 +122,13 @@ func handler(w http.ResponseWriter, r *http.Request) { } for _, route := range routeMap { if strings.HasPrefix(r.URL.Path, route.Path) { - r.URL.Path = strings.TrimPrefix(r.URL.Path, route.Path) - // log.Printf("[Route] %s", route.Url.String()) - - proxy := httputil.NewSingleHostReverseProxy(&route.Url) - proxy.ServeHTTP(w, r) + realPath := strings.TrimPrefix(r.URL.Path, route.Path) + origHost := r.Host + r.URL.Path = realPath + log.Printf("[Route] %s -> %s%s ", origHost, route.Url.String(), route.Path) + proxyServer := httputil.NewSingleHostReverseProxy(&route.Url) + proxyServer.Transport = transport + proxyServer.ServeHTTP(w, r) return } } @@ -136,20 +157,31 @@ func buildContainerCfg(container types.Container) { prop.Set(reflect.ValueOf(value)) } } - if config.Scheme != "http" && config.Scheme != "https" { - log.Printf("%s: unsupported scheme: %s, using http", container_name, config.Scheme) - config.Scheme = "http" - } if config.Port == "" { for _, port := range container.Ports { + // set first, but keep trying config.Port = fmt.Sprintf("%d", port.PrivatePort) - break + // until we find 80 or 8080 + if port.PrivatePort == 80 || port.PrivatePort == 8080 { + break + } } } if config.Port == "" { // no ports exposed or specified return } + if config.Scheme == "" { + if strings.HasSuffix(config.Port, "443") { + config.Scheme = "https" + } else { + config.Scheme = "http" + } + } + if config.Scheme != "http" && config.Scheme != "https" { + log.Printf("%s: unsupported scheme: %s, using http", container_name, config.Scheme) + config.Scheme = "http" + } if config.Host == "" { if container.HostConfig.NetworkMode != "host" { config.Host = container_name @@ -161,7 +193,11 @@ func buildContainerCfg(container types.Container) { if !inMap { subdomainRouteMap[alias] = make([]Route, 0) } - route := Route{Url: url.URL{Scheme: config.Scheme, Host: fmt.Sprintf("%s:%s", config.Host, config.Port)}, Path: config.Path} + url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port)) + if err != nil { + log.Fatal(err) + } + route := Route{Url: *url, Path: config.Path} subdomainRouteMap[alias] = append(subdomainRouteMap[alias], route) } } @@ -174,5 +210,4 @@ func buildRoutes() { for _, container := range containerSlice { buildContainerCfg(container) } - // log.Println(subdomainRouteMap) }