added load balance support and verbose level

This commit is contained in:
yusing 2024-03-06 12:34:06 +08:00
parent a5c53a4f4f
commit 2f439233ed
25 changed files with 530 additions and 240 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
compose.yml compose.yml
go-proxy.yml go-proxy.yml
bin/go-proxy.bak
logs/

10
Dockerfile Executable file → Normal file
View file

@ -2,14 +2,18 @@ FROM alpine:latest
LABEL maintainer="yusing@6uo.me" LABEL maintainer="yusing@6uo.me"
COPY bin/go-proxy /usr/bin RUN apk add --no-cache bash
RUN mkdir /app
COPY bin/go-proxy entrypoint.sh /app/
COPY templates/ /app/templates COPY templates/ /app/templates
RUN chmod +rx /usr/bin/go-proxy RUN chmod +x /app/go-proxy /app/entrypoint.sh
ENV DOCKER_HOST unix:///var/run/docker.sock ENV DOCKER_HOST unix:///var/run/docker.sock
ENV VERBOSITY=1
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
EXPOSE 8443 EXPOSE 8443
CMD ["go-proxy"] WORKDIR /app
ENTRYPOINT /app/entrypoint.sh

16
Makefile Normal file → Executable file
View file

@ -1,6 +1,6 @@
.PHONY: build up restart logs get test-udp-container .PHONY: all build up quick-restart restart logs get udp-server
all: build up logs all: build quick-restart logs
build: build:
mkdir -p bin mkdir -p bin
@ -9,12 +9,18 @@ build:
up: up:
docker compose up -d --build go-proxy docker compose up -d --build go-proxy
quick-restart: # quick restart without restarting the container
docker cp bin/go-proxy go-proxy:/app/go-proxy
docker cp templates/* go-proxy:/app/templates
docker cp entrypoint.sh go-proxy:/app/entrypoint.sh
docker exec -d go-proxy bash -c "/app/entrypoint.sh restart"
restart: restart:
docker compose down -t 0 docker kill go-proxy
docker compose up -d docker compose up -d go-proxy
logs: logs:
docker compose logs -f docker logs -f go-proxy
get: get:
go get -d -u ./src/go-proxy go get -d -u ./src/go-proxy

View file

@ -13,6 +13,7 @@ In the examples domain `x.y.z` is used, replace them with your domain
- [Single Port Configuration](#single-port-configuration-example) - [Single Port Configuration](#single-port-configuration-example)
- [Multiple Ports Configuration](#multiple-ports-configuration-example) - [Multiple Ports Configuration](#multiple-ports-configuration-example)
- [TCP/UDP Configuration](#tcpudp-configuration-example) - [TCP/UDP Configuration](#tcpudp-configuration-example)
- [Load balancing Configuration](#load-balancing-configuration-example)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Memory usage](#memory-usage) - [Memory usage](#memory-usage)
@ -25,6 +26,7 @@ In the examples domain `x.y.z` is used, replace them with your domain
- path matching - path matching
- HTTP proxy - HTTP proxy
- TCP/UDP Proxy (experimental, unable to release port on hot-reload) - TCP/UDP Proxy (experimental, unable to release port on hot-reload)
- HTTP round robin load balance support (same subdomain and path across containers replicas)
- Auto hot-reload when container start / die / stop. - Auto hot-reload when container start / die / stop.
- Simple panel to see all reverse proxies and health (visit port [panel port] of go-proxy `https://*.y.z:[panel port]`) - Simple panel to see all reverse proxies and health (visit port [panel port] of go-proxy `https://*.y.z:[panel port]`)
@ -62,6 +64,11 @@ In the examples domain `x.y.z` is used, replace them with your domain
8. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies 8. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies
## Known issues
- When a container has replicas, you have to specify `proxy.<alias>.host` to the container_name
- UDP proxy does not work properly
## Configuration ## Configuration
With container name, no label needs to be added. With container name, no label needs to be added.
@ -81,6 +88,19 @@ However, there are some labels you can manipulate with:
- `targetPort` must be a number, or the predefined names (see [stream.go](src/go-proxy/stream.go#L28)) - `targetPort` must be a number, or the predefined names (see [stream.go](src/go-proxy/stream.go#L28))
- `proxy.<alias>.path`: path matching (for http proxy only) - `proxy.<alias>.path`: path matching (for http proxy only)
- defaults to empty - defaults to empty
- `proxy.<alias>.path_mode`: mode for path handling
- defaults to empty
- allowed: \<empty>, forward, sub
- empty: remove path prefix from URL when proxying
1. apps.y.z/webdav -> webdav:80
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
- forward: path remain unchanged
1. apps.y.z/webdav -> webdav:80/webdav
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
- sub: remove path prefix from both URL and HTML attributes (`src`, `href` and `action`)
- `proxy.<alias>.load_balance`: enable load balance
- allowed: `1`, `true`
### Single port configuration example ### Single port configuration example
@ -109,9 +129,9 @@ minio:
container_name: minio container_name: minio
... ...
labels: labels:
proxy.aliases: minio,minio-console - proxy.aliases=minio,minio-console
proxy.minio.port: 9000 - proxy.minio.port=9000
proxy.minio-console.port: 9001 - proxy.minio-console.port=9001
# visit https://minio.y.z to access minio # visit https://minio.y.z to access minio
# visit https://minio-console.y.z/whoami to access minio console # visit https://minio-console.y.z/whoami to access minio console
@ -144,6 +164,18 @@ go-proxy:
# access app-db via <*>.y.z:20000 # access app-db via <*>.y.z:20000
``` ```
## Load balancing Configuration Example
```yaml
nginx:
...
deploy:
mode: replicated
replicas: 3
labels:
- proxy.nginx.load_balance=1 # allowed: [1, true]
```
## Troubleshooting ## Troubleshooting
Q: How to fix when it shows "no matching route for subdomain \<subdomain>"? Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?

Binary file not shown.

View file

@ -3,9 +3,13 @@ services:
app: app:
build: . build: .
container_name: go-proxy container_name: go-proxy
hostname: go-proxy # set hostname to prevent adding itself to proxy list
restart: always restart: always
networks: # also add here networks: # ^also add here
- default - default
environment:
- VERBOSITY=1 # LOG LEVEL (optional, defaults to 1)
- DEBUG=1 # (optional enable only for debug)
ports: ports:
- 80:80 # http - 80:80 # http
- 443:443 # https - 443:443 # https
@ -15,14 +19,15 @@ services:
volumes: volumes:
- /path/to/cert.pem:/certs/cert.crt:ro - /path/to/cert.pem:/certs/cert.crt:ro
- /path/to/privkey.pem:/certs/priv.key:ro - /path/to/privkey.pem:/certs/priv.key:ro
- ./go-proxy/logs:/app/log # path to logs
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
extra_hosts: extra_hosts:
- host.docker.internal:host-gateway - host.docker.internal:host-gateway # required if you have containers in `host` network_mode
logging: logging:
driver: 'json-file' driver: 'json-file'
options: options:
max-file: '1' max-file: '1'
max-size: 128k max-size: 128k
networks: # you may add other external networks networks: # ^you may add other external networks
default: default:
driver: bridge driver: bridge

12
entrypoint.sh Normal file
View file

@ -0,0 +1,12 @@
#!/bin/bash
if [ "$1" == "restart" ]; then
killall go-proxy
fi
if [ "$DEBUG" == "1" ]; then
/app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 &
if [ "$1" != "restart" ]; then
tail -f /dev/null
fi
else
/app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 &
fi

15
go.mod
View file

@ -2,10 +2,9 @@ module github.com/yusing/go-proxy
go 1.21.7 go 1.21.7
require ( require github.com/docker/docker v25.0.3+incompatible
github.com/docker/docker v25.0.3+incompatible
golang.org/x/text v0.14.0 require github.com/golang/glog v1.2.0
)
require ( require (
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
@ -13,9 +12,9 @@ require (
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect go.opentelemetry.io/otel/sdk v1.24.0 // indirect
golang.org/x/mod v0.15.0 // indirect golang.org/x/mod v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.18.0 // indirect golang.org/x/tools v0.19.0 // indirect
gotest.tools/v3 v3.5.1 // indirect gotest.tools/v3 v3.5.1 // indirect
) )
@ -35,6 +34,6 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.21.0 golang.org/x/net v0.22.0
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.18.0 // indirect
) )

10
go.sum
View file

@ -25,6 +25,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -74,12 +76,16 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -90,6 +96,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
@ -102,6 +110,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

57
src/go-proxy/constants.go Normal file
View file

@ -0,0 +1,57 @@
package main
import "time"
var (
ImageNamePortMap = map[string]string{
"postgres": "5432",
"mysql": "3306",
"mariadb": "3306",
"redis": "6379",
"mssql": "1433",
"memcached": "11211",
"rabbitmq": "5672",
"mongo": "27017",
}
ExtraNamePortMap = map[string]string{
"dns": "53",
"ssh": "22",
"ftp": "21",
"smtp": "25",
"pop3": "110",
"imap": "143",
}
NamePortMap = func() map[string]string {
m := make(map[string]string)
for k, v := range ImageNamePortMap {
m[k] = v
}
for k, v := range ExtraNamePortMap {
m[k] = v
}
return m
}()
)
var (
StreamSchemes = []string{TCPStreamType, UDPStreamType} // TODO: support "tcp:udp", "udp:tcp"
HTTPSchemes = []string{"http", "https"}
ValidSchemes = append(StreamSchemes, HTTPSchemes...)
)
const (
UDPStreamType = "udp"
TCPStreamType = "tcp"
)
const (
ProxyPathMode_Forward = "forward"
ProxyPathMode_Sub = "sub" // TODO: implement
ProxyPathMode_RemovedPath = ""
)
const StreamStopListenTimeout = 1 * time.Second
const templateFile = "/app/templates/panel.html"
const udpBufferSize = 1500

74
src/go-proxy/docker.go Normal file → Executable file
View file

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"reflect" "reflect"
"sort" "sort"
@ -12,28 +11,10 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/golang/glog"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
type ProxyConfig struct {
id string
Alias string
Scheme string
Host string
Port string
Path string // http proxy only
}
func NewProxyConfig() ProxyConfig {
return ProxyConfig{}
}
func (cfg *ProxyConfig) UpdateId() {
cfg.id = fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
}
var dockerClient *client.Client var dockerClient *client.Client
func buildContainerRoute(container types.Container) { func buildContainerRoute(container types.Container) {
@ -54,8 +35,12 @@ func buildContainerRoute(container types.Container) {
for label, value := range container.Labels { for label, value := range container.Labels {
if strings.HasPrefix(label, prefix) { if strings.HasPrefix(label, prefix) {
field := strings.TrimPrefix(label, prefix) field := strings.TrimPrefix(label, prefix)
field = cases.Title(language.Und, cases.NoLower).String(field) field = utils.snakeToCamel(field)
prop := reflect.ValueOf(&config).Elem().FieldByName(field) prop := reflect.ValueOf(&config).Elem().FieldByName(field)
if prop.Kind() == 0 {
glog.Infof("[Build] %s: ignoring unknown field %s", alias, field)
continue
}
prop.Set(reflect.ValueOf(value)) prop.Set(reflect.ValueOf(value))
} }
} }
@ -76,6 +61,7 @@ func buildContainerRoute(container types.Container) {
} }
if config.Port == "" { if config.Port == "" {
// no ports exposed or specified // no ports exposed or specified
glog.Infof("[Build] %s has no port exposed", alias)
return return
} }
if config.Scheme == "" { if config.Scheme == "" {
@ -87,7 +73,7 @@ func buildContainerRoute(container types.Container) {
imageSplit := strings.Split(container.Image, "/") imageSplit := strings.Split(container.Image, "/")
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":") imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
imageName := imageSplit[0] imageName := imageSplit[0]
_, isKnownImage := imageNamePortMap[imageName] _, isKnownImage := ImageNamePortMap[imageName]
if isKnownImage { if isKnownImage {
config.Scheme = "tcp" config.Scheme = "tcp"
} else { } else {
@ -96,22 +82,37 @@ func buildContainerRoute(container types.Container) {
} }
} }
if !isValidScheme(config.Scheme) { if !isValidScheme(config.Scheme) {
log.Printf("%s: unsupported scheme: %s, using http", container_name, config.Scheme) glog.Infof("%s: unsupported scheme: %s, using http", container_name, config.Scheme)
config.Scheme = "http" config.Scheme = "http"
} }
if config.Host == "" { if config.Host == "" {
if container.HostConfig.NetworkMode != "host" { switch {
config.Host = container_name case container.HostConfig.NetworkMode == "host":
} else {
config.Host = "host.docker.internal" config.Host = "host.docker.internal"
case config.LoadBalance == "true":
case config.LoadBalance == "1":
for _, network := range container.NetworkSettings.Networks {
config.Host = network.IPAddress
break
}
default:
for _, network := range container.NetworkSettings.Networks {
for _, alias := range network.Aliases {
config.Host = alias
break
}
}
} }
} }
if config.Host == "" {
config.Host = container_name
}
config.Alias = alias config.Alias = alias
config.UpdateId() config.UpdateId()
wg.Add(1) wg.Add(1)
go func() { go func() {
createRoute(&config) CreateRoute(&config)
wg.Done() wg.Done()
}() }()
} }
@ -119,10 +120,10 @@ func buildContainerRoute(container types.Container) {
} }
func buildRoutes() { func buildRoutes() {
initRoutes() InitRoutes()
containerSlice, err := dockerClient.ContainerList(context.Background(), container.ListOptions{}) containerSlice, err := dockerClient.ContainerList(context.Background(), container.ListOptions{})
if err != nil { if err != nil {
log.Fatal(err) glog.Fatal(err)
} }
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
@ -130,22 +131,9 @@ func buildRoutes() {
} }
for _, container := range containerSlice { for _, container := range containerSlice {
if container.Names[0] == hostname { // skip self if container.Names[0] == hostname { // skip self
glog.Infof("[Build] Skipping %s", container.Names[0])
continue continue
} }
buildContainerRoute(container) buildContainerRoute(container)
} }
} }
func findHTTPRoute(host string, path string) (*HTTPRoute, error) {
subdomain := strings.Split(host, ".")[0]
routeMap, ok := routes.HTTPRoutes.TryGet(subdomain)
if !ok {
return nil, fmt.Errorf("no matching route for subdomain %s", subdomain)
}
for _, route := range routeMap {
if strings.HasPrefix(path, route.Path) {
return &route, nil
}
}
return nil, fmt.Errorf("no matching route for path %s for subdomain %s", path, subdomain)
}

22
src/go-proxy/http_lbpool.go Executable file
View file

@ -0,0 +1,22 @@
package main
import "sync/atomic"
type httpLoadBalancePool struct {
pool []*HTTPRoute
curentIndex atomic.Int32
}
func NewHTTPLoadBalancePool() *httpLoadBalancePool {
return &httpLoadBalancePool{
pool: make([]*HTTPRoute, 0),
}
}
func (p *httpLoadBalancePool) Add(route *HTTPRoute) {
p.pool = append(p.pool, route)
}
func (p *httpLoadBalancePool) Iterator() []*HTTPRoute {
return p.pool
}

View file

@ -1,66 +0,0 @@
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
)
type HTTPRoute struct {
Url *url.URL
Path string
Proxy *httputil.ReverseProxy
}
// TODO: default + per proxy
var transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 60 * time.Second,
KeepAlive: 60 * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ForceAttemptHTTP2: true,
}
func NewHTTPRoute(Url *url.URL, Path string) HTTPRoute {
proxy := httputil.NewSingleHostReverseProxy(Url)
proxy.Transport = transport
return HTTPRoute{Url: Url, Path: Path, Proxy: proxy}
}
func redirectToTLS(w http.ResponseWriter, r *http.Request) {
// Redirect to the same host but with HTTPS
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 httpProxyHandler(w http.ResponseWriter, r *http.Request) {
route, err := findHTTPRoute(r.Host, r.URL.Path)
if err != nil {
log.Printf("[Request] failed %s %s%s, error: %v",
r.Method,
r.Host,
r.URL.Path,
err,
)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
route.Proxy.ServeHTTP(w, r)
}

160
src/go-proxy/http_route.go Executable file
View file

@ -0,0 +1,160 @@
package main
import (
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/golang/glog"
)
type HTTPRoute struct {
Url *url.URL
Path string
PathMode string
Proxy *httputil.ReverseProxy
}
func isValidProxyPathMode(mode string) bool {
switch mode {
case ProxyPathMode_Forward, ProxyPathMode_Sub, ProxyPathMode_RemovedPath:
return true
default:
return false
}
}
func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port))
if err != nil {
glog.Infoln(err)
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.Transport = transport
if !isValidProxyPathMode(config.PathMode) {
return nil, fmt.Errorf("invalid path mode: %s", config.PathMode)
}
route := &HTTPRoute{
Url: url,
Path: config.Path,
Proxy: proxy,
PathMode: config.PathMode,
}
proxy.Director = nil
initRewrite := func(pr *httputil.ProxyRequest) {
pr.SetURL(url)
pr.SetXForwarded()
}
rewrite := initRewrite
switch {
case config.Path == "", config.PathMode == ProxyPathMode_Forward:
break
case config.PathMode == ProxyPathMode_Sub:
rewrite = func(pr *httputil.ProxyRequest) {
initRewrite(pr)
// disable compression
pr.Out.Header.Set("Accept-Encoding", "identity")
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
}
route.Proxy.ModifyResponse = func(r *http.Response) error {
contentType, ok := r.Header["Content-Type"]
if !ok || len(contentType) == 0 {
glog.Infof("unknown content type for %s", r.Request.URL.String())
return nil
}
if !strings.HasPrefix(contentType[0], "text/html") {
return nil
}
err := utils.respRemovePath(r, config.Path)
if err != nil {
err = fmt.Errorf("failed to remove path prefix %s: %v", config.Path, err)
r.Status = err.Error()
r.StatusCode = http.StatusInternalServerError
}
return err
}
default:
rewrite = func(pr *httputil.ProxyRequest) {
initRewrite(pr)
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
}
}
if glog.V(3) {
route.Proxy.Rewrite = func(pr *httputil.ProxyRequest) {
rewrite(pr)
r := pr.In
glog.Infof("[Request] %s %s%s", r.Method, r.Host, r.URL.Path)
glog.V(4).InfoDepthf(1, "Headers: %v", r.Header)
}
} else {
route.Proxy.Rewrite = rewrite
}
return route, nil
}
func (p *httpLoadBalancePool) Pick() *HTTPRoute {
// round-robin
index := int(p.curentIndex.Load())
defer p.curentIndex.Add(1)
return p.pool[index%len(p.pool)]
}
func redirectToTLS(w http.ResponseWriter, r *http.Request) {
// Redirect to the same host but with 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 findHTTPRoute(host string, path string) (*HTTPRoute, error) {
subdomain := strings.Split(host, ".")[0]
routeMap, ok := routes.HTTPRoutes.UnsafeGet(subdomain)
if !ok {
return nil, fmt.Errorf("no matching route for subdomain %s", subdomain)
}
return routeMap.FindMatch(path)
}
func httpProxyHandler(w http.ResponseWriter, r *http.Request) {
route, err := findHTTPRoute(r.Host, r.URL.Path)
if err != nil {
err = fmt.Errorf("[Request] failed %s %s%s, error: %v",
r.Method,
r.Host,
r.URL.Path,
err,
)
glog.Error(err)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
route.Proxy.ServeHTTP(w, r)
}
// TODO: default + per proxy
var transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 60 * time.Second,
KeepAlive: 60 * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
}

32
src/go-proxy/main.go Normal file → Executable file
View file

@ -1,28 +1,30 @@
package main package main
import ( import (
"log" "flag"
"net/http" "net/http"
"runtime" "runtime"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/golang/glog"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func main() { func main() {
var err error var err error
flag.Parse()
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil { if err != nil {
log.Fatal(err) glog.Fatal(err)
} }
buildRoutes() buildRoutes()
log.Printf("[Build] built %v reverse proxies", countRoutes()) glog.Infof("[Build] built %v reverse proxies", CountRoutes())
beginListenStreams() BeginListenStreams()
go func() { go func() {
filter := filters.NewArgs( filter := filters.NewArgs(
@ -37,13 +39,13 @@ func main() {
select { select {
case msg := <-msgChan: case msg := <-msgChan:
// TODO: handle actor only // TODO: handle actor only
log.Printf("[Event] %s %s caused rebuild", msg.Action, msg.Actor.Attributes["name"]) glog.Infof("[Event] %s %s caused rebuild", msg.Action, msg.Actor.Attributes["name"])
endListenStreams() EndListenStreams()
buildRoutes() buildRoutes()
log.Printf("[Build] rebuilt %v reverse proxies", countRoutes()) glog.Infof("[Build] rebuilt %v reverse proxies", CountRoutes())
beginListenStreams() BeginListenStreams()
case err := <-errChan: case err := <-errChan:
log.Printf("[Event] %s", err) glog.Infof("[Event] %s", err)
msgChan, errChan = dockerClient.Events(context.Background(), types.EventsOptions{Filters: filter}) msgChan, errChan = dockerClient.Events(context.Background(), types.EventsOptions{Filters: filter})
} }
} }
@ -53,22 +55,22 @@ func main() {
mux.HandleFunc("/", httpProxyHandler) mux.HandleFunc("/", httpProxyHandler)
go func() { go func() {
log.Println("Starting HTTP server on port 80") glog.Infoln("Starting HTTP server on port 80")
err := http.ListenAndServe(":80", http.HandlerFunc(redirectToTLS)) err := http.ListenAndServe(":80", http.HandlerFunc(redirectToTLS))
if err != nil { if err != nil {
log.Fatal("HTTP server error", err) glog.Fatal("HTTP server error", err)
} }
}() }()
go func() { go func() {
log.Println("Starting HTTPS panel on port 8443") glog.Infoln("Starting HTTPS panel on port 8443")
err := http.ListenAndServeTLS(":8443", "/certs/cert.crt", "/certs/priv.key", http.HandlerFunc(panelHandler)) err := http.ListenAndServeTLS(":8443", "/certs/cert.crt", "/certs/priv.key", http.HandlerFunc(panelHandler))
if err != nil { if err != nil {
log.Fatal("HTTP server error", err) glog.Fatal("HTTP server error", err)
} }
}() }()
log.Println("Starting HTTPS server on port 443") glog.Infoln("Starting HTTPS server on port 443")
err = http.ListenAndServeTLS(":443", "/certs/cert.crt", "/certs/priv.key", mux) err = http.ListenAndServeTLS(":443", "/certs/cert.crt", "/certs/priv.key", mux)
if err != nil { if err != nil {
log.Fatal("HTTPS Server error: ", err) glog.Fatal("HTTPS Server error: ", err)
} }
} }

12
src/go-proxy/map.go Normal file → Executable file
View file

@ -16,19 +16,19 @@ type SafeMapInterface[KT comparable, VT interface{}] interface {
type SafeMap[KT comparable, VT interface{}] struct { type SafeMap[KT comparable, VT interface{}] struct {
SafeMapInterface[KT, VT] SafeMapInterface[KT, VT]
m map[KT]VT m map[KT]VT
mutex sync.Mutex mutex sync.Mutex
defaultFactory func() VT defaultFactory func() VT
} }
func NewSafeMap[KT comparable, VT interface{}](df... func() VT) *SafeMap[KT, VT] { func NewSafeMap[KT comparable, VT interface{}](df ...func() VT) *SafeMap[KT, VT] {
if len(df) == 0 { if len(df) == 0 {
return &SafeMap[KT, VT]{ return &SafeMap[KT, VT]{
m: make(map[KT]VT), m: make(map[KT]VT),
} }
} }
return &SafeMap[KT, VT]{ return &SafeMap[KT, VT]{
m: make(map[KT]VT), m: make(map[KT]VT),
defaultFactory: df[0], defaultFactory: df[0],
} }
} }
@ -54,10 +54,8 @@ func (m *SafeMap[KT, VT]) Get(key KT) VT {
return value return value
} }
func (m *SafeMap[KT, VT]) TryGet(key KT) (VT, bool) { func (m *SafeMap[KT, VT]) UnsafeGet(key KT) (VT, bool) {
m.mutex.Lock()
value, ok := m.m[key] value, ok := m.m[key]
m.mutex.Unlock()
return value, ok return value, ok
} }

7
src/go-proxy/panel.go Normal file → Executable file
View file

@ -2,14 +2,13 @@ package main
import ( import (
"html/template" "html/template"
"log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
)
const templateFile = "/app/templates/panel.html" "github.com/golang/glog"
)
var healthCheckHttpClient = &http.Client{ var healthCheckHttpClient = &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@ -72,7 +71,7 @@ func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) {
url, err := url.Parse(targetUrl) url, err := url.Parse(targetUrl)
if err != nil { if err != nil {
log.Printf("[Panel] failed to parse %s, error: %v", targetUrl, err) glog.Infof("[Panel] failed to parse %s, error: %v", targetUrl, err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }

View file

@ -0,0 +1,30 @@
package main
import (
"fmt"
"strings"
)
type pathPoolMap struct {
*SafeMap[string, *httpLoadBalancePool]
}
func newPathPoolMap() pathPoolMap {
return pathPoolMap{
NewSafeMap[string](NewHTTPLoadBalancePool),
}
}
func (m pathPoolMap) Add(path string, route *HTTPRoute) {
m.Ensure(path)
m.Get(path).Add(route)
}
func (m pathPoolMap) FindMatch(pathGot string) (*HTTPRoute, error) {
for pathWant, v := range m.m {
if strings.HasPrefix(pathGot, pathWant) {
return v.Pick(), nil
}
}
return nil, fmt.Errorf("no matching route for path %s", pathGot)
}

View file

@ -0,0 +1,22 @@
package main
import "fmt"
type ProxyConfig struct {
id string
Alias string
Scheme string
Host string
Port string
LoadBalance string
Path string // http proxy only
PathMode string // http proxy only
}
func NewProxyConfig() ProxyConfig {
return ProxyConfig{}
}
func (cfg *ProxyConfig) UpdateId() {
cfg.id = fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
}

39
src/go-proxy/route.go Normal file → Executable file
View file

@ -1,27 +1,21 @@
package main package main
import ( import (
"fmt"
"log"
"net/url"
"sync" "sync"
"github.com/golang/glog"
) )
type Routes struct { type Routes struct {
HTTPRoutes *SafeMap[string, []HTTPRoute] // id -> path HTTPRoutes *SafeMap[string, pathPoolMap] // id -> (path -> routes)
StreamRoutes *SafeMap[string, StreamRoute] // id -> target StreamRoutes *SafeMap[string, StreamRoute] // id -> target
Mutex sync.Mutex Mutex sync.Mutex
} }
var routes = Routes{} var routes = Routes{}
var streamSchemes = []string{"tcp", "udp"} // TODO: support "tcp:udp", "udp:tcp"
var httpSchemes = []string{"http", "https"}
var validSchemes = append(streamSchemes, httpSchemes...)
func isValidScheme(scheme string) bool { func isValidScheme(scheme string) bool {
for _, v := range validSchemes { for _, v := range ValidSchemes {
if v == scheme { if v == scheme {
return true return true
} }
@ -30,7 +24,7 @@ func isValidScheme(scheme string) bool {
} }
func isStreamScheme(scheme string) bool { func isStreamScheme(scheme string) bool {
for _, v := range streamSchemes { for _, v := range StreamSchemes {
if v == scheme { if v == scheme {
return true return true
} }
@ -38,40 +32,35 @@ func isStreamScheme(scheme string) bool {
return false return false
} }
func initRoutes() { func InitRoutes() {
utils.resetPortsInUse() utils.resetPortsInUse()
routes.HTTPRoutes = NewSafeMap[string, []HTTPRoute]( routes.HTTPRoutes = NewSafeMap[string](newPathPoolMap)
func() []HTTPRoute {
return make([]HTTPRoute, 0)
},
)
routes.StreamRoutes = NewSafeMap[string, StreamRoute]() routes.StreamRoutes = NewSafeMap[string, StreamRoute]()
} }
func countRoutes() int { func CountRoutes() int {
return routes.HTTPRoutes.Size() + routes.StreamRoutes.Size() return routes.HTTPRoutes.Size() + routes.StreamRoutes.Size()
} }
func createRoute(config *ProxyConfig) { func CreateRoute(config *ProxyConfig) {
if isStreamScheme(config.Scheme) { if isStreamScheme(config.Scheme) {
if routes.StreamRoutes.Contains(config.id) { if routes.StreamRoutes.Contains(config.id) {
log.Printf("[Build] Duplicated %s stream %s, ignoring", config.Scheme, config.id) glog.Infof("[Build] Duplicated %s stream %s, ignoring", config.Scheme, config.id)
return return
} }
route, err := NewStreamRoute(config) route, err := NewStreamRoute(config)
if err != nil { if err != nil {
log.Println(err) glog.Infoln(err)
return return
} }
routes.StreamRoutes.Set(config.id, route) routes.StreamRoutes.Set(config.id, route)
} else { } else {
routes.HTTPRoutes.Ensure(config.Alias) routes.HTTPRoutes.Ensure(config.Alias)
url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port)) route, err := NewHTTPRoute(config)
if err != nil { if err != nil {
log.Println(err) glog.Infoln(err)
return return
} }
route := NewHTTPRoute(url, config.Path) routes.HTTPRoutes.Get(config.Alias).Add(config.Path, route)
routes.HTTPRoutes.Set(config.Alias, append(routes.HTTPRoutes.Get(config.Alias), route))
} }
} }

61
src/go-proxy/stream.go → src/go-proxy/stream_route.go Normal file → Executable file
View file

@ -3,11 +3,12 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/golang/glog"
) )
type StreamRoute interface { type StreamRoute interface {
@ -46,7 +47,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
port_split := strings.Split(config.Port, ":") port_split := strings.Split(config.Port, ":")
if len(port_split) != 2 { if len(port_split) != 2 {
log.Printf(`[Build] %s: Invalid stream port %s, `+ glog.Infof(`[Build] %s: Invalid stream port %s, `+
`assuming it's targetPort`, config.Alias, config.Port) `assuming it's targetPort`, config.Alias, config.Port)
srcPort = "0" srcPort = "0"
dstPort = config.Port dstPort = config.Port
@ -55,7 +56,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
dstPort = port_split[1] dstPort = port_split[1]
} }
port, hasName := namePortMap[dstPort] port, hasName := NamePortMap[dstPort]
if hasName { if hasName {
dstPort = port dstPort = port
} }
@ -117,11 +118,16 @@ func (route *StreamRouteBase) PrintError(err error) {
if err == nil { if err == nil {
return return
} }
route.Logf("Error: %s", err.Error()) glog.Errorf("[%s -> %s] %s: %v",
route.ListeningScheme,
route.TargetScheme,
route.Alias,
err,
)
} }
func (route *StreamRouteBase) Logf(format string, v ...interface{}) { func (route *StreamRouteBase) Logf(format string, v ...interface{}) {
log.Printf("[%s -> %s] %s: "+format, glog.Infof("[%s -> %s] %s: "+format,
append([]interface{}{ append([]interface{}{
route.ListeningScheme, route.ListeningScheme,
route.TargetScheme, route.TargetScheme,
@ -176,14 +182,14 @@ func stopListening(route StreamRoute) {
case <-done: case <-done:
route.Logf("Stopped listening") route.Logf("Stopped listening")
return return
case <-time.After(streamStopListenTimeout): case <-time.After(StreamStopListenTimeout):
route.Logf("timed out waiting for connections") route.Logf("timed out waiting for connections")
return return
} }
} }
func allStreamsDo(msg string, fn ...func(StreamRoute)) { func allStreamsDo(msg string, fn ...func(StreamRoute)) {
log.Printf("[Stream] %s", msg) glog.Infof("[Stream] %s", msg)
var wg sync.WaitGroup var wg sync.WaitGroup
@ -198,48 +204,13 @@ func allStreamsDo(msg string, fn ...func(StreamRoute)) {
} }
wg.Wait() wg.Wait()
log.Printf("[Stream] Finished %s", msg) glog.Infof("[Stream] Finished %s", msg)
} }
func beginListenStreams() { func BeginListenStreams() {
allStreamsDo("Start", StreamRoute.SetupListen, StreamRoute.Listen) allStreamsDo("Start", StreamRoute.SetupListen, StreamRoute.Listen)
} }
func endListenStreams() { func EndListenStreams() {
allStreamsDo("Stop", StreamRoute.StopListening) allStreamsDo("Stop", StreamRoute.StopListening)
} }
var imageNamePortMap = map[string]string{
"postgres": "5432",
"mysql": "3306",
"mariadb": "3306",
"redis": "6379",
"mssql": "1433",
"memcached": "11211",
"rabbitmq": "5672",
"mongo": "27017",
}
var extraNamePortMap = map[string]string{
"dns": "53",
"ssh": "22",
"ftp": "21",
"smtp": "25",
"pop3": "110",
"imap": "143",
}
var namePortMap = func() map[string]string {
m := make(map[string]string)
for k, v := range imageNamePortMap {
m[k] = v
}
for k, v := range extraNamePortMap {
m[k] = v
}
return m
}()
const UDPStreamType = "udp"
const TCPStreamType = "tcp"
// const maxQueueSizePerStream = 100
const streamStopListenTimeout = 1 * time.Second

5
src/go-proxy/tcp.go → src/go-proxy/tcp_route.go Normal file → Executable file
View file

@ -4,10 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/golang/glog"
) )
const tcpDialTimeout = 5 * time.Second const tcpDialTimeout = 5 * time.Second
@ -100,7 +101,7 @@ func (route *TCPRoute) grHandleConnection(clientConn net.Conn) {
dialer := &net.Dialer{} dialer := &net.Dialer{}
serverConn, err := dialer.DialContext(ctx, route.TargetScheme, serverAddr) serverConn, err := dialer.DialContext(ctx, route.TargetScheme, serverAddr)
if err != nil { if err != nil {
log.Printf("[Stream Dial] %v", err) glog.Infof("[Stream Dial] %v", err)
return return
} }
route.tcpPipe(clientConn, serverConn) route.tcpPipe(clientConn, serverConn)

5
src/go-proxy/udp.go → src/go-proxy/udp_route.go Normal file → Executable file
View file

@ -7,11 +7,6 @@ import (
"sync" "sync"
) )
const udpBufferSize = 1500
// const udpListenTimeout = 100 * time.Second
// const udpConnectionTimeout = 30 * time.Second
type UDPRoute struct { type UDPRoute struct {
*StreamRouteBase *StreamRouteBase

50
src/go-proxy/utils.go Normal file → Executable file
View file

@ -1,11 +1,16 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
xhtml "golang.org/x/net/html"
) )
type Utils struct { type Utils struct {
@ -82,3 +87,48 @@ func (*Utils) healthCheckStream(scheme string, host string) error {
conn.Close() conn.Close()
return nil return nil
} }
func (*Utils) snakeToCamel(s string) string {
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
return strings.ReplaceAll(toHyphenCamel, "-", "")
}
func htmlNodesSubPath(node *xhtml.Node, path string) {
if node.Type == xhtml.ElementNode {
for _, attr := range node.Attr {
switch attr.Key {
case "src": // img, script, etc.
case "href": // link
case "action": // form
if strings.HasPrefix(attr.Val, path) {
attr.Val = strings.Replace(attr.Val, path, "", 1)
}
}
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
htmlNodesSubPath(c, path)
}
}
func (*Utils) respRemovePath(r *http.Response, path string) error {
// remove all path prefix from relative path in script, img, a, ...
doc, err := xhtml.Parse(r.Body)
if err != nil {
return err
}
htmlNodesSubPath(doc, path)
var buf bytes.Buffer
err = xhtml.Render(&buf, doc)
if err != nil {
return err
}
r.Body = io.NopCloser(strings.NewReader(buf.String()))
return nil
}

8
templates/panel.html Normal file → Executable file
View file

@ -105,11 +105,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range $alias, $httpRoutes := .HTTPRoutes.Iterator}} {{range $alias, $pathPoolMap := .HTTPRoutes.Iterator}}
{{range $route := $httpRoutes}} {{range $path, $lbPool := $pathPoolMap.Iterator}}
{{range $_, $route := $lbPool.Iterator}}
<tr> <tr>
<td>{{$alias}}</td> <td>{{$alias}}</td>
<td>{{$route.Path}}</td> <td>{{$path}}</td>
<td>{{$route.Url.String}}</td> <td>{{$route.Url.String}}</td>
<td class="align-middle"> <td class="align-middle">
<div class="health-circle"></div> <div class="health-circle"></div>
@ -117,6 +118,7 @@
</tr> </tr>
{{end}} {{end}}
{{end}} {{end}}
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>