mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
README update, https redirect fix, increased timeout
This commit is contained in:
parent
acad8dc5ba
commit
88f704563c
5 changed files with 94 additions and 54 deletions
0
.vscode/settings.json
vendored
Normal file → Executable file
0
.vscode/settings.json
vendored
Normal file → Executable file
57
README.md
Normal file → Executable file
57
README.md
Normal file → Executable 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
2
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
|
||||
)
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
85
main.go
85
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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue