README update, https redirect fix, increased timeout

This commit is contained in:
yusing 2024-03-01 10:29:45 +08:00
parent acad8dc5ba
commit 88f704563c
5 changed files with 94 additions and 54 deletions

0
.vscode/settings.json vendored Normal file → Executable file
View file

57
README.md Normal file → Executable file
View file

@ -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.<alias>.host` is reachable
3. Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.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

2
go.mod
View file

@ -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
)

4
go.sum
View file

@ -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=

85
main.go
View file

@ -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)
}