Merge branch 'dev' into 'main'

Dev

See merge request yusing/go-proxy!3
This commit is contained in:
yusing w 2024-04-08 05:07:27 +00:00
commit e12b356d0d
36 changed files with 1268 additions and 554 deletions

View file

@ -8,7 +8,13 @@ jobs:
build_and_push: build_and_push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set tags (latest)
if: "!endsWith(github.ref, '-dev')"
run: echo "::set-output name=tag::latest,${{ github.ref_name }}"
- name: Set tags (dev)
if: "endsWith(github.ref, '-dev')"
run: echo "::set-output name=tag::dev,${{ github.ref_name }}"
- name: Build and Push Container to ghcr.io - name: Build and Push Container to ghcr.io
uses: GlueOps/github-actions-build-push-containers@v0.3.7 uses: GlueOps/github-actions-build-push-containers@v0.3.7
with: with:
tags: latest,${{ github.ref_name }} tags: ${{ steps.build_and_push.outputs.tag }}

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ templates/codemirror/
logs/ logs/
log/ log/
.vscode/settings.json

15
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,15 @@
build-image:
image: docker
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:latest
- if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
script:
- echo building $CI_REGISTRY_IMAGE
- docker build --pull -t $CI_REGISTRY_IMAGE .
- docker push $CI_REGISTRY_IMAGE

12
.vscode/settings.example.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
]
}
}

16
.vscode/settings.json vendored
View file

@ -1,16 +0,0 @@
{
"go.inferGopath": false,
"yaml.schemas": {
// "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
// "config.example.yml",
// "config.yml"
// ],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
],
"file:///config/workspace/go-proxy/schema/config.schema.json": [
"file:///config/workspace/go-proxy/config.example.yml"
]
}
}

View file

@ -3,7 +3,7 @@ RUN apk add --no-cache unzip wget make
COPY Makefile . COPY Makefile .
RUN make setup-codemirror RUN make setup-codemirror
FROM golang:1.22.1-alpine as builder FROM golang:1.22.2-alpine as builder
COPY src/ /src COPY src/ /src
COPY go.mod go.sum /src/go-proxy COPY go.mod go.sum /src/go-proxy
WORKDIR /src/go-proxy WORKDIR /src/go-proxy

View file

@ -18,12 +18,14 @@ build:
mkdir -p bin mkdir -p bin
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
test:
go test src/go-proxy/*.go
up: up:
docker compose up -d --build app docker compose up -d
restart: restart:
docker kill go-proxy docker compose restart -t 0
docker compose up -d app
logs: logs:
tail -f log/go-proxy.log tail -f log/go-proxy.log
@ -31,6 +33,12 @@ logs:
get: get:
go get -d -u ./src/go-proxy go get -d -u ./src/go-proxy
repush:
git reset --soft HEAD^
git add -A
git commit -m "repush"
git push gitlab dev --force
udp-server: udp-server:
docker run -it --rm \ docker run -it --rm \
-p 9999:9999/udp \ -p 9999:9999/udp \

201
README.md
View file

@ -6,7 +6,7 @@ In the examples domain `x.y.z` is used, replace them with your domain
## Table of content ## Table of content
- [go-proxy](#go-proxy) <!-- TOC -->
- [Table of content](#table-of-content) - [Table of content](#table-of-content)
- [Key Points](#key-points) - [Key Points](#key-points)
- [How to use](#how-to-use) - [How to use](#how-to-use)
@ -17,24 +17,18 @@ In the examples domain `x.y.z` is used, replace them with your domain
- [Command-line args](#command-line-args) - [Command-line args](#command-line-args)
- [Commands](#commands) - [Commands](#commands)
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode) - [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
- [Configuration](#configuration)
- [Labels (docker)](#labels-docker)
- [Environment variables](#environment-variables) - [Environment variables](#environment-variables)
- [Config File](#config-file) - [Config File](#config-file)
- [Fields](#fields) - [Fields](#fields)
- [Provider Kinds](#provider-kinds) - [Provider Kinds](#provider-kinds)
- [Provider File](#provider-file) - [Provider File](#provider-file)
- [Supported DNS Challenge Providers](#supported-dns-challenge-providers) - [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- [Examples](#examples)
- [Single port configuration example](#single-port-configuration-example)
- [Multiple ports configuration example](#multiple-ports-configuration-example)
- [TCP/UDP configuration example](#tcpudp-configuration-example)
- [Load balancing Configuration Example](#load-balancing-configuration-example)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Benchmarks](#benchmarks) - [Benchmarks](#benchmarks)
- [Known issues](#known-issues) - [Known issues](#known-issues)
- [Memory usage](#memory-usage) - [Memory usage](#memory-usage)
- [Build it yourself](#build-it-yourself) - [Build it yourself](#build-it-yourself)
<!-- /TOC -->
## Key Points ## Key Points
@ -58,6 +52,8 @@ In the examples domain `x.y.z` is used, replace them with your domain
![config editor screenshot](screenshots/config_editor.png) ![config editor screenshot](screenshots/config_editor.png)
[🔼Back to top](#table-of-content)
## How to use ## How to use
1. Setup DNS Records to your machine's IP address 1. Setup DNS Records to your machine's IP address
@ -65,18 +61,23 @@ In the examples domain `x.y.z` is used, replace them with your domain
- A Record: `*.y.z` -> `10.0.10.1` - A Record: `*.y.z` -> `10.0.10.1`
- AAAA Record: `*.y.z` -> `::ffff:a00:a01` - AAAA Record: `*.y.z` -> `::ffff:a00:a01`
2. Start `go-proxy` (see [Binary](docs/binary.md) or [docker](docs/docker.md)) 2. Start `go-proxy` by
- [Running from binary or as a system service](docs/binary.md)
- [Running as a docker container](docs/docker.md)
3. Start editing config files 3. Start editing config files
- with text editor (i.e. Visual Studio Code) - with text editor (i.e. Visual Studio Code)
- or with web config editor by navigate to `ip:8080` - or with web config editor by navigate to `http://ip:8080`
[🔼Back to top](#table-of-content)
## Tested Services ## Tested Services
### HTTP/HTTPs Reverse Proxy ### HTTP/HTTPs Reverse Proxy
- nginx - Nginx
- minio - Minio
- AdguardHome Dashboard - AdguardHome Dashboard
- etc. - etc.
@ -91,6 +92,8 @@ In the examples domain `x.y.z` is used, replace them with your domain
- Adguardhome DNS - Adguardhome DNS
- Palworld Dedicated Server - Palworld Dedicated Server
[🔼Back to top](#table-of-content)
## Command-line args ## Command-line args
`go-proxy [command]` `go-proxy [command]`
@ -106,9 +109,11 @@ Examples:
- Binary: `go-proxy reload` - Binary: `go-proxy reload`
- Docker: `docker exec -it go-proxy /app/go-proxy reload` - Docker: `docker exec -it go-proxy /app/go-proxy reload`
[🔼Back to top](#table-of-content)
## Use JSON Schema in VSCode ## Use JSON Schema in VSCode
Modify `.vscode/settings.json` to fit your needs Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify to fit your needs
```json ```json
{ {
@ -125,78 +130,36 @@ Modify `.vscode/settings.json` to fit your needs
} }
``` ```
## Configuration [🔼Back to top](#table-of-content)
With container name, no label needs to be added _(most of the time)_. ## Environment variables
### Labels (docker)
See [docker.md](docs/docker.md#docker-compose-example) for examples
When `go-proxy` is running in `host` network mode, see [here](docs/docker.md#docker-compose-example-host-network) for extra instructions
- `proxy.aliases`: comma separated aliases for subdomain matching
- default: container name
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
- allowed: `http`, `https`, `tcp`, `udp`
- `host`: proxy host
- default: `container_name`
- `port`: proxy port
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
- `http(s)`: number in range og `0 - 65535`
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
- `no_tls_verify`: whether skip tls verify when scheme is https
- default: `false`
- `path`: proxy path _(http(s) proxy only)_
- default: empty
- `path_mode`: mode for path handling
- default: 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`: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"`
- `load_balance`: _(Docker only)_ enable load balance
- allowed: `1`, `true`
### Environment variables
- `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.) - `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.)
- `GOPROXY_HOST_NETWORK`: _(Docker only)_ set to `1` when `network_mode: host` - `GOPROXY_HOST_NETWORK`: _(Docker only)_ set to `1` when `network_mode: host`
- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation on config load / reload **(for testing new DNS Challenge providers)**
### Config File [🔼Back to top](#table-of-content)
## Config File
See [config.example.yml](config.example.yml) for more See [config.example.yml](config.example.yml) for more
#### Fields ### Fields
- `autocert`: autocert configuration - `autocert`: autocert configuration
- `email`: ACME Email - `email`: ACME Email
- `domains`: a list of domains for cert registration - `domains`: a list of domains for cert registration
- `provider`: DNS Challenge provider, see [Supported DNS Challenge Providers](#supported-dns-challenge-providers) - `provider`: DNS Challenge provider, see [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- `options`: provider specific options - `options`: [provider specific options](#supported-dns-challenge-providers)
- `providers`: reverse proxy providers configuration - `providers`: reverse proxy providers configuration
- `kind`: provider kind (string), see [Provider Kinds](#provider-kinds) - `kind`: provider kind (string), see [Provider Kinds](#provider-kinds)
- `value`: provider specific value - `value`: provider specific value
#### Provider Kinds [🔼Back to top](#table-of-content)
### Provider Kinds
- `docker`: load reverse proxies from docker - `docker`: load reverse proxies from docker
@ -209,12 +172,16 @@ See [config.example.yml](config.example.yml) for more
value: relative path of file to `config/` value: relative path of file to `config/`
[🔼Back to top](#table-of-content)
### Provider File ### Provider File
Fields are same as [docker labels](#labels-docker) starting from `scheme` Fields are same as [docker labels](docs/docker.md#labels) starting from `scheme`
See [providers.example.yml](providers.example.yml) for examples See [providers.example.yml](providers.example.yml) for examples
[🔼Back to top](#table-of-content)
### Supported DNS Challenge Providers ### Supported DNS Challenge Providers
- Cloudflare - Cloudflare
@ -223,85 +190,19 @@ See [providers.example.yml](providers.example.yml) for examples
Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions
- CloudDNS
- `client_id`
- `email`
- `password`
- DuckDNS (thanks [earvingad](https://github.com/earvingad))
- `token`: DuckDNS Token
To add more provider support, see [this](docs/add_dns_provider.md) To add more provider support, see [this](docs/add_dns_provider.md)
## Examples [🔼Back to top](#table-of-content)
See [docker.md](docs/docker.md#docker-compose-example) for complete examples
### Single port configuration example
```yaml
# (default) https://<container_name>.y.z
whoami:
image: traefik/whoami
container_name: whoami # => whoami.y.z
# enable both subdomain and path matching:
whoami:
image: traefik/whoami
container_name: whoami
labels:
- proxy.aliases=whoami,apps
- proxy.apps.path=/whoami
# 1. visit https://whoami.y.z
# 2. visit https://apps.y.z/whoami
```
### Multiple ports configuration example
```yaml
minio:
image: quay.io/minio/minio
container_name: minio
...
labels:
- proxy.aliases=minio,minio-console
- proxy.minio.port=9000
- proxy.minio-console.port=9001
# visit https://minio.y.z to access minio
# visit https://minio-console.y.z/whoami to access minio console
```
### TCP/UDP configuration example
```yaml
# In the app
app-db:
image: postgres:15
container_name: app-db
...
labels:
# Optional (postgres is in the known image map)
- proxy.app-db.scheme=tcp
# Optional (first free port will be used for listening port)
- proxy.app-db.port=20000:postgres
# In go-proxy
go-proxy:
...
ports:
- 80:80
...
- <your desired port>:20000/tcp
# or 20000-20010:20000-20010/tcp to declare large range at once
# 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
@ -309,6 +210,8 @@ Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?
A: Make sure the container is running, and \<subdomain> matches any container name / alias A: Make sure the container is running, and \<subdomain> matches any container name / alias
[🔼Back to top](#table-of-content)
## Benchmarks ## Benchmarks
Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint
@ -414,14 +317,20 @@ Local benchmark (client running wrk and `go-proxy` server are under same proxmox
Transfer/sec: 10.94MB Transfer/sec: 10.94MB
``` ```
[🔼Back to top](#table-of-content)
## Known issues ## Known issues
None - Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
[🔼Back to top](#table-of-content)
## Memory usage ## Memory usage
It takes ~15 MB for 50 proxy entries It takes ~15 MB for 50 proxy entries
[🔼Back to top](#table-of-content)
## Build it yourself ## Build it yourself
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already 1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
@ -433,3 +342,5 @@ It takes ~15 MB for 50 proxy entries
4. build binary with `make build` 4. build binary with `make build`
5. start your container with `make up` (docker) or `bin/go-proxy` (binary) 5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
[🔼Back to top](#table-of-content)

View file

@ -37,5 +37,5 @@
password: b9841238feb177a84330f password: b9841238feb177a84330f
``` ```
5. Run and test if it works 5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works
6. Commit and create pull request 6. Commit and create pull request

View file

@ -1,4 +1,24 @@
# Getting started with `go-proxy` docker container # Docker container guide
## Table of content
<!-- TOC -->
- [Table of content](#table-of-content)
- [Setup](#setup)
- [Labels](#labels)
- [Labels (docker specific)](#labels-docker-specific)
- [Troubleshooting](#troubleshooting)
- [Docker compose examples](#docker-compose-examples)
- [Local docker provider in bridge network](#local-docker-provider-in-bridge-network)
- [Remote docker provider](#remote-docker-provider)
- [Explaination](#explaination)
- [Remote setup](#remote-setup)
- [Proxy setup](#proxy-setup)
- [Local docker provider in host network](#local-docker-provider-in-host-network)
- [Proxy setup](#proxy-setup)
- [Services URLs for above examples](#services-urls-for-above-examples)
<!-- /TOC -->
## Setup ## Setup
@ -46,6 +66,77 @@
7. Start editing config files in `http://<ip>:8080` 7. Start editing config files in `http://<ip>:8080`
[🔼Back to top](#table-of-content)
## Labels
- `proxy.aliases`: comma separated aliases for subdomain matching
- default: container name
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
- allowed: `http`, `https`, `tcp`, `udp`
- `host`: proxy host
- default: `container_name`
- `port`: proxy port
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
- `http(s)`: number in range og `0 - 65535`
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
- `no_tls_verify`: whether skip tls verify when scheme is https
- default: `false`
- `path`: proxy path _(http(s) proxy only)_
- default: empty
- `path_mode`: mode for path handling
- default: 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`: **(experimental)** remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"`
- `set_headers`: a list of header to set, (key:value, one by line)
Duplicated keys will be treated as multiple-value headers
```yaml
labels:
- |
proxy.app.set_headers=
X-Custom-Header1: value1
X-Custom-Header1: value2
X-Custom-Header2: value2
```
- `hide_headers`: comma seperated list of headers to hide
[🔼Back to top](#table-of-content)
## Labels (docker specific)
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.app.headers.hide: X-Powered-By,X-Custom-Header`)
- `headers.set.<header>`: value of header to set
- `headers.hide`: comma seperated list of headers to hide
- `load_balance`: enable load balance
- allowed: `1`, `true`
[🔼Back to top](#table-of-content)
## Troubleshooting ## Troubleshooting
- Firewall issues - Firewall issues
@ -64,7 +155,11 @@
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -` `docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
## Docker compose example (bridge network) [🔼Back to top](#table-of-content)
## Docker compose examples
### Local docker provider in bridge network
```yaml ```yaml
volumes: volumes:
@ -136,28 +231,19 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
labels: labels:
- proxy.aliases=gp - proxy.aliases=gp
- proxy.panel.port=8080 - proxy.gp.port=8080
``` ```
### Services URLs [🔼Back to top](#table-of-content)
- `gp.yourdomain.com`: go-proxy web panel ### Remote docker provider
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
- `adg.yourdomain.com`: adguard dashboard
- `nginx.yourdomain.com`: nginx
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server
- `yourdomain.com:8211`: palworld server
## Docker compose example (host network) #### Explaination
### Notice - Expose container ports to random port in remote host
- Use container port with an asterisk sign **(\*)** before to find remote port automatically
When `go-proxy` is running in `host` network mode, you must: #### Remote setup
- set `GOPROXY_HOST_NETWORK=1`
- map ports to host explicitly
- add an asterisk sign **(*)** before `port` number under `labels`
```yaml ```yaml
volumes: volumes:
@ -170,7 +256,7 @@ services:
adg: adg:
image: adguard/adguardhome image: adguard/adguardhome
restart: unless-stopped restart: unless-stopped
ports: # map random ports to container ports ports: # map container ports
- 80 - 80
- 3000 - 3000
- 53/udp - 53/udp
@ -224,19 +310,62 @@ services:
- 80 - 80
volumes: volumes:
- nginx:/usr/share/nginx/html - nginx:/usr/share/nginx/html
```
[🔼Back to top](#table-of-content)
#### Proxy setup
```yaml
go-proxy: go-proxy:
image: ghcr.io/yusing/go-proxy image: ghcr.io/yusing/go-proxy
container_name: go-proxy container_name: go-proxy
restart: always restart: always
network_mode: host # no port mapping needed for host network mode network_mode: host
environment:
- GOPROXY_HOST_NETWORK=1 # required for host network mode
volumes: volumes:
- ./config:/app/config - ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
labels: labels:
- proxy.aliases=gp - proxy.aliases=gp
- proxy.panel.port=808 - proxy.gp.port=8080
``` ```
**Same services URLs as [`bridge`](#services-urls) example!** [🔼Back to top](#table-of-content)
### Local docker provider in host network
Mostly as remote docker setup, see [remote setup](#remote-setup)
With `GOPROXY_HOST_NETWORK=1` to treat it as remote docker provider
#### Proxy setup
```yaml
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
network_mode: host
environment: # this part is needed for local docker in host mode
- GOPROXY_HOST_NETWORK=1
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.gp.port=8080
```
[🔼Back to top](#table-of-content)
### Services URLs for above examples
- `gp.yourdomain.com`: go-proxy web panel
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
- `adg.yourdomain.com`: adguard dashboard
- `nginx.yourdomain.com`: nginx
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server
- `yourdomain.com:8211`: palworld server
[🔼Back to top](#table-of-content)

25
go.mod
View file

@ -9,14 +9,14 @@ require (
github.com/go-acme/lego/v4 v4.16.1 github.com/go-acme/lego/v4 v4.16.1
github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
golang.org/x/net v0.22.0 golang.org/x/net v0.24.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/cloudflare-go v0.86.0 // indirect github.com/cloudflare/cloudflare-go v0.92.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
@ -37,17 +37,18 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel v1.25.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/metric v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.25.0 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect golang.org/x/tools v0.20.0 // indirect
gotest.tools/v3 v3.5.1 // indirect gotest.tools/v3 v3.5.1 // indirect
) )

62
go.sum
View file

@ -1,11 +1,11 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI= github.com/cloudflare/cloudflare-go v0.92.0 h1:ltJvGvqZ4G6Fm2hHOYZ5RWpJQcrM0oDrsjjZydZhFJQ=
github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw= github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -58,7 +58,6 @@ github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/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=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -79,7 +78,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -88,61 +86,57 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.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=
@ -153,8 +147,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
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=

View file

@ -1,16 +1,26 @@
example: # matching `app.y.z` example: # matching `app.y.z`
# optional, defaults to http # optional, defaults to http
scheme: scheme: http
# required, proxy target # required, proxy target
host: 10.0.0.1 host: 10.0.0.1
# optional, defaults to 80 for http, 443 for https # optional, defaults to 80 for http, 443 for https
port: 80 port: "80"
# optional, defaults to empty # optional, defaults to empty
path: path:
# optional, defaults to sub # optional, defaults to empty
path_mode: path_mode:
# optional (https only) # optional (https only)
# no_tls_verify: false # no_tls_verify: false
# optional headers to set / override (http(s) only)
set_headers:
HEADER_A:
- VALUE_1
- VALUE_2
HEADER_B: [VALUE_3]
# optional headers to hide (http(s) only)
hide_headers:
- HEADER_C
- HEADER_D
app1: # matching `app1.y.z` -> http://x.y.z app1: # matching `app1.y.z` -> http://x.y.z
host: x.y.z host: x.y.z
app2: # `app2` has no effect for tcp / udp, but still has to be unique across files app2: # `app2` has no effect for tcp / udp, but still has to be unique across files
@ -23,3 +33,6 @@ app3: # matching `app3.y.z` -> https://10.0.0.1/app3
path: /app3 path: /app3
path_mode: forward path_mode: forward
no_tls_verify: false no_tls_verify: false
set_headers:
X-Forwarded-Proto: [https]
X-Forwarded-Host: [app3.y.z]

View file

@ -24,11 +24,28 @@
"provider": { "provider": {
"description": "DNS Challenge Provider", "description": "DNS Challenge Provider",
"type": "string", "type": "string",
"enum": ["cloudflare"] "enum": ["cloudflare", "clouddns", "duckdns"]
}, },
"options": { "options": {
"description": "Provider specific options", "description": "Provider specific options",
"type": "object", "type": "object"
}
},
"required": ["email", "domains", "provider", "options"],
"allOf": [
{
"if": {
"properties": {
"provider": {
"const": "cloudflare"
}
}
},
"then": {
"properties": {
"options": {
"required": ["auth_token"],
"additionalProperties": false,
"properties": { "properties": {
"auth_token": { "auth_token": {
"description": "Cloudflare API Token with Zone Scope", "description": "Cloudflare API Token with Zone Scope",
@ -36,16 +53,60 @@
} }
} }
} }
}
}
}, },
"required": ["email", "domains", "provider", "options"],
"anyOf": [
{ {
"if": {
"properties": { "properties": {
"provider": { "provider": {
"const": "cloudflare" "const": "clouddns"
}
}
}, },
"then": {
"properties": {
"options": { "options": {
"required": ["auth_token"] "required": ["client_id", "email", "password"],
"additionalProperties": false,
"properties": {
"client_id": {
"description": "CloudDNS Client ID",
"type": "string"
},
"email": {
"description": "CloudDNS Email",
"type": "string"
},
"password": {
"description": "CloudDNS Password",
"type": "string"
}
}
}
}
}
},
{
"if": {
"properties": {
"provider": {
"const": "duckdns"
}
}
},
"then": {
"properties": {
"options": {
"required": ["token"],
"additionalProperties": false,
"properties": {
"token": {
"description": "DuckDNS Token",
"type": "string"
}
}
}
} }
} }
} }

View file

@ -55,7 +55,9 @@
"no_tls_verify": { "no_tls_verify": {
"description": "Disable TLS verification for https proxy", "description": "Disable TLS verification for https proxy",
"type": "boolean" "type": "boolean"
} },
"set_headers": {},
"hide_headers": {}
}, },
"required": ["host"], "required": ["host"],
"additionalProperties": false, "additionalProperties": false,
@ -129,6 +131,23 @@
"type": "null" "type": "null"
} }
] ]
},
"set_headers": {
"type": "object",
"description": "Proxy headers to set",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"hide_headers": {
"type":"array",
"description": "Proxy headers to hide",
"items": {
"type": "string"
}
} }
} }
}, },
@ -145,6 +164,12 @@
}, },
"path_mode": { "path_mode": {
"not": true "not": true
},
"set_headers": {
"not": true
},
"hide_headers": {
"not": true
} }
}, },
"required": ["port"] "required": ["port"]

View file

@ -98,7 +98,7 @@ Wants=network-online.target systemd-networkd-wait-online.service
Type=simple Type=simple
ExecStart=${APP_ROOT}/bin/go-proxy ExecStart=${APP_ROOT}/bin/go-proxy
WorkingDirectory=${APP_ROOT} WorkingDirectory=${APP_ROOT}
Environment="IS_SYSTEMD=1" Environment="GOPROXY_IS_SYSTEMD=1"
Restart=on-failure Restart=on-failure
RestartSec=1s RestartSec=1s
KillMode=process KillMode=process

View file

@ -9,6 +9,7 @@ import (
"crypto/x509" "crypto/x509"
"os" "os"
"path" "path"
"slices"
"sync" "sync"
"time" "time"
@ -18,6 +19,7 @@ import (
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns/clouddns" "github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/duckdns"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
) )
@ -54,7 +56,7 @@ type AutoCertProvider interface {
GetExpiries() CertExpiries GetExpiries() CertExpiries
LoadCert() bool LoadCert() bool
ObtainCert() NestedErrorLike ObtainCert() NestedErrorLike
RenewalOn() time.Time ShouldRenewOn() time.Time
ScheduleRenewal() ScheduleRenewal()
} }
@ -72,7 +74,7 @@ func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) {
} }
gen, ok := providersGenMap[cfg.Provider] gen, ok := providersGenMap[cfg.Provider]
if !ok { if !ok {
ne.Extraf("unknown provider: %s", cfg.Provider) ne.Extraf("unknown provider: %q", cfg.Provider)
} }
if ne.HasExtras() { if ne.HasExtras() {
return nil, ne return nil, ne
@ -189,13 +191,9 @@ func (p *autoCertProvider) LoadCert() bool {
return true return true
} }
func (p *autoCertProvider) RenewalOn() time.Time { func (p *autoCertProvider) ShouldRenewOn() time.Time {
t := time.Now().AddDate(0, 0, 3)
for _, expiry := range p.certExpiries { for _, expiry := range p.certExpiries {
if expiry.Before(t) { return expiry.AddDate(0, -1, 0)
return time.Now()
}
return t
} }
// this line should never be reached // this line should never be reached
panic("no certificate available") panic("no certificate available")
@ -203,8 +201,8 @@ func (p *autoCertProvider) RenewalOn() time.Time {
func (p *autoCertProvider) ScheduleRenewal() { func (p *autoCertProvider) ScheduleRenewal() {
for { for {
t := time.Until(p.RenewalOn()) t := time.Until(p.ShouldRenewOn())
aclog.Infof("next renewal in %v", t) aclog.Infof("next renewal in %v", t.Round(time.Second))
time.Sleep(t) time.Sleep(t)
err := p.renewIfNeeded() err := p.renewIfNeeded()
if err != nil { if err != nil {
@ -230,7 +228,29 @@ func (p *autoCertProvider) saveCert(cert *certificate.Resource) NestedErrorLike
} }
func (p *autoCertProvider) needRenewal() bool { func (p *autoCertProvider) needRenewal() bool {
return time.Now().After(p.RenewalOn()) expired := time.Now().After(p.ShouldRenewOn())
if expired {
return true
}
if len(p.cfg.Domains) != len(p.certExpiries) {
return true
}
wantedDomains := make([]string, len(p.cfg.Domains))
certDomains := make([]string, len(p.certExpiries))
copy(wantedDomains, p.cfg.Domains)
i := 0
for domain := range p.certExpiries {
certDomains[i] = domain
i++
}
slices.Sort(wantedDomains)
slices.Sort(certDomains)
for i, domain := range certDomains {
if domain != wantedDomains[i] {
return true
}
}
return false
} }
func (p *autoCertProvider) renewIfNeeded() NestedErrorLike { func (p *autoCertProvider) renewIfNeeded() NestedErrorLike {
@ -249,6 +269,7 @@ func (p *autoCertProvider) renewIfNeeded() NestedErrorLike {
for { for {
err := p.ObtainCert() err := p.ObtainCert()
if err == nil { if err == nil {
aclog.Info("renewed certificate")
return nil return nil
} }
trials++ trials++
@ -306,4 +327,5 @@ func setOptions[T interface{}](cfg *T, opt ProviderOptions) error {
var providersGenMap = map[string]ProviderGenerator{ var providersGenMap = map[string]ProviderGenerator{
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), "cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), "clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
"duckdns": providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
} }

View file

@ -1,10 +1,10 @@
package main package main
import ( import (
"os"
"sync" "sync"
"time" "time"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -25,12 +25,10 @@ type Config interface {
func NewConfig(path string) Config { func NewConfig(path string) Config {
cfg := &config{ cfg := &config{
reader: &FileReader{Path: path}, reader: &FileReader{Path: path},
l: cfgl,
} }
cfg.watcher = NewFileWatcher( // must init fields above before creating watcher
path, cfg.watcher = cfg.NewFileWatcher()
cfg.MustReload, // OnChange
func() { os.Exit(1) }, // OnDelete
)
return cfg return cfg
} }
@ -43,10 +41,7 @@ func (cfg *config) Value() configModel {
return *cfg.m return *cfg.m
} }
func (cfg *config) Load(reader ...Reader) error { func (cfg *config) Load() error {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
if cfg.reader == nil { if cfg.reader == nil {
panic("config reader not set") panic("config reader not set")
} }
@ -68,7 +63,7 @@ func (cfg *config) Load(reader ...Reader) error {
ne.With(err) ne.With(err)
} }
pErrs := NewNestedError("errors in these providers") pErrs := NewNestedError("these providers have errors")
for name, p := range model.Providers { for name, p := range model.Providers {
if p.Kind != ProviderKind_File { if p.Kind != ProviderKind_File {
@ -90,13 +85,16 @@ func (cfg *config) Load(reader ...Reader) error {
return ne return ne
} }
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
cfg.m = model cfg.m = model
return nil return nil
} }
func (cfg *config) MustLoad() { func (cfg *config) MustLoad() {
if err := cfg.Load(); err != nil { if err := cfg.Load(); err != nil {
cfgl.Fatal(err) cfg.l.Fatal(err)
} }
} }
@ -115,7 +113,7 @@ func (cfg *config) Reload() error {
func (cfg *config) MustReload() { func (cfg *config) MustReload() {
if err := cfg.Reload(); err != nil { if err := cfg.Reload(); err != nil {
cfgl.Fatal(err) cfg.l.Fatal(err)
} }
} }
@ -144,7 +142,7 @@ func (cfg *config) StartProviders() {
cfg.providerInitialized = true cfg.providerInitialized = true
if pErrs.HasExtras() { if pErrs.HasExtras() {
cfgl.Error(pErrs) cfg.l.Error(pErrs)
} }
} }
@ -194,6 +192,7 @@ func defaultConfig() *configModel {
type config struct { type config struct {
m *configModel m *configModel
l logrus.FieldLogger
reader Reader reader Reader
watcher Watcher watcher Watcher
mutex sync.Mutex mutex sync.Mutex

View file

@ -123,7 +123,7 @@ var (
} }
) )
const wildcardLabelPrefix = "proxy.*." const wildcardAlias = "*"
const clientUrlFromEnv = "FROM_ENV" const clientUrlFromEnv = "FROM_ENV"
@ -147,18 +147,6 @@ const (
var ( var (
configSchema *jsonschema.Schema configSchema *jsonschema.Schema
providersSchema *jsonschema.Schema providersSchema *jsonschema.Schema
_ = func() *jsonschema.Compiler {
c := jsonschema.NewCompiler()
c.Draft = jsonschema.Draft7
var err error
if configSchema, err = c.Compile(configSchemaPath); err != nil {
panic(err)
}
if providersSchema, err = c.Compile(providersSchemaPath); err != nil {
panic(err)
}
return c
}()
) )
const ( const (
@ -168,17 +156,36 @@ const (
const udpBufferSize = 1500 const udpBufferSize = 1500
var isHostNetworkMode = os.Getenv("GOPROXY_HOST_NETWORK") == "1" var isHostNetworkMode = getEnvBool("GOPROXY_HOST_NETWORK")
var logLevel = func() logrus.Level { var logLevel = func() logrus.Level {
switch os.Getenv("GOPROXY_DEBUG") { if getEnvBool("GOPROXY_DEBUG") {
case "1", "true":
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
} }
return logrus.GetLevel() return logrus.GetLevel()
}() }()
var isRunningAsService = func() bool { var isRunningAsService = getEnvBool("IS_SYSTEMD") || getEnvBool("GOPROXY_IS_SYSTEMD") // IS_SYSTEMD is deprecated
v := os.Getenv("IS_SYSTEMD")
return v == "1" var noSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
}()
func getEnvBool(key string) bool {
v := os.Getenv(key)
return v == "1" || v == "true"
}
func initSchema() {
if noSchemaValidation {
return
}
c := jsonschema.NewCompiler()
c.Draft = jsonschema.Draft7
var err error
if configSchema, err = c.Compile(configSchemaPath); err != nil {
panic(err)
}
if providersSchema, err = c.Compile(providersSchemaPath); err != nil {
panic(err)
}
}

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -14,20 +15,15 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func (p *Provider) setConfigField(c *ProxyConfig, label string, value string, prefix string) error { func setConfigField(pl *ProxyLabel, c *ProxyConfig) error {
if strings.HasPrefix(label, prefix) { return setFieldFromSnake(c, pl.Field, pl.Value)
field := strings.TrimPrefix(label, prefix)
if err := setFieldFromSnake(c, field, value); err != nil {
return err
}
}
return nil
} }
func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) ProxyConfigSlice { func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) (ProxyConfigSlice, error) {
var aliases []string var aliases []string
cfgs := make(ProxyConfigSlice, 0) cfgs := make(ProxyConfigSlice, 0)
cfgMap := make(map[string]*ProxyConfig)
containerName := strings.TrimPrefix(container.Names[0], "/") containerName := strings.TrimPrefix(container.Names[0], "/")
aliasesLabel, ok := container.Labels["proxy.aliases"] aliasesLabel, ok := container.Labels["proxy.aliases"]
@ -35,7 +31,8 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
if !ok { if !ok {
aliases = []string{containerName} aliases = []string{containerName}
} else { } else {
aliases = strings.Split(aliasesLabel, ",") v, _ := commaSepParser(aliasesLabel)
aliases = v.([]string)
} }
if clientIP == "" && isHostNetworkMode { if clientIP == "" && isHostNetworkMode {
@ -44,21 +41,42 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
isRemote := clientIP != "" isRemote := clientIP != ""
for _, alias := range aliases { for _, alias := range aliases {
ne := NewNestedError("invalid label config").Subjectf("container %s", containerName) cfgMap[alias] = &ProxyConfig{}
}
ne := NewNestedError("these labels have errors").Subject(containerName)
l := p.l.WithField("container", containerName).WithField("alias", alias)
config := NewProxyConfig(p)
prefix := fmt.Sprintf("proxy.%s.", alias)
for label, value := range container.Labels { for label, value := range container.Labels {
err := p.setConfigField(&config, label, value, prefix) pl, err := parseProxyLabel(label, value)
if err != nil { if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias)) if !errors.Is(err, errNotProxyLabel) {
ne.ExtraError(NewNestedErrorFrom(err).Subject(label))
} }
err = p.setConfigField(&config, label, value, wildcardLabelPrefix) continue
}
if pl.Alias == wildcardAlias {
for alias := range cfgMap {
pl.Alias = alias
err = setConfigField(pl, cfgMap[alias])
if err != nil { if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias)) ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias))
} }
} }
continue
}
config, ok := cfgMap[pl.Alias]
if !ok {
ne.ExtraError(NewNestedError("unknown alias").Subject(pl.Alias))
continue
}
err = setConfigField(pl, config)
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias))
}
}
for alias, config := range cfgMap {
l := p.l.WithField("alias", alias)
if config.Port == "" { if config.Port == "" {
config.Port = fmt.Sprintf("%d", selectPort(container, isRemote)) config.Port = fmt.Sprintf("%d", selectPort(container, isRemote))
} }
@ -70,8 +88,6 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
switch { switch {
case strings.HasSuffix(config.Port, "443"): case strings.HasSuffix(config.Port, "443"):
config.Scheme = "https" config.Scheme = "https"
case strings.HasPrefix(container.Image, "sha256:"):
config.Scheme = "http"
default: default:
imageName := getImageName(container) imageName := getImageName(container)
_, isKnownImage := ImageNamePortMapTCP[imageName] _, isKnownImage := ImageNamePortMapTCP[imageName]
@ -99,7 +115,6 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
} }
} }
if config.Host == "" { if config.Host == "" {
switch { switch {
case isRemote: case isRemote:
@ -126,12 +141,15 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
config.Alias = alias config.Alias = alias
if ne.HasExtras() { if ne.HasExtras() {
l.Error(ne)
continue continue
} }
cfgs = append(cfgs, config) cfgs = append(cfgs, *config)
} }
return cfgs
if ne.HasExtras() {
return nil, ne
}
return cfgs, nil
} }
func (p *Provider) getDockerClient() (*client.Client, error) { func (p *Provider) getDockerClient() (*client.Client, error) {
@ -196,8 +214,19 @@ func (p *Provider) getDockerProxyConfigs() (ProxyConfigSlice, error) {
cfgs := make(ProxyConfigSlice, 0) cfgs := make(ProxyConfigSlice, 0)
ne := NewNestedError("these containers have errors")
for _, container := range containerSlice { for _, container := range containerSlice {
cfgs = append(cfgs, p.getContainerProxyConfigs(&container, clientIP)...) ccfgs, err := p.getContainerProxyConfigs(&container, clientIP)
if err != nil {
ne.ExtraError(err)
continue
}
cfgs = append(cfgs, ccfgs...)
}
if ne.HasExtras() {
// print but ignore
p.l.Error(ne)
} }
return cfgs, nil return cfgs, nil

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -46,6 +47,10 @@ func NewNestedErrorFrom(err error) NestedErrorLike {
if err == nil { if err == nil {
panic("cannot convert nil error to NestedError") panic("cannot convert nil error to NestedError")
} }
errUnwrap := errors.Unwrap(err)
if errUnwrap != nil {
return NewNestedErrorFrom(errUnwrap)
}
return NewNestedError(err.Error()) return NewNestedError(err.Error())
} }
@ -92,23 +97,23 @@ func (ne *NestedError) Level() int {
return ne.level return ne.level
} }
func (ef *NestedError) Error() string { func (ne *NestedError) Error() string {
var buf strings.Builder var buf strings.Builder
ef.writeToSB(&buf, "") ne.writeToSB(&buf, ne.level, "")
return buf.String() return buf.String()
} }
func (ef *NestedError) HasInner() bool { func (ne *NestedError) HasInner() bool {
return ef.inner != nil return ne.inner != nil
} }
func (ef *NestedError) HasExtras() bool { func (ne *NestedError) HasExtras() bool {
return len(ef.extras) > 0 return len(ne.extras) > 0
} }
func (ef *NestedError) With(inner error) NestedErrorLike { func (ne *NestedError) With(inner error) NestedErrorLike {
ef.Lock() ne.Lock()
defer ef.Unlock() defer ne.Unlock()
var in *NestedError var in *NestedError
@ -116,79 +121,75 @@ func (ef *NestedError) With(inner error) NestedErrorLike {
case NestedErrorLike: case NestedErrorLike:
in = t.copy() in = t.copy()
default: default:
in = &NestedError{extras: []string{t.Error()}} in = &NestedError{message: t.Error()}
} }
if ef.inner == nil { if ne.inner == nil {
ef.inner = in ne.inner = in
} else { } else {
ef.inner.ExtraError(in) ne.inner.ExtraError(in)
} }
root := ef root := ne
for root.inner != nil { for root.inner != nil {
root.inner.level = root.level + 1 root.inner.level = root.level + 1
root = root.inner root = root.inner
} }
return ef return ne
} }
func (ef *NestedError) addLevel(level int) NestedErrorLike { func (ne *NestedError) addLevel(level int) NestedErrorLike {
ef.level += level ne.level += level
if ef.inner != nil { if ne.inner != nil {
ef.inner.addLevel(level) ne.inner.addLevel(level)
} }
return ef return ne
} }
func (ef *NestedError) copy() *NestedError { func (ne *NestedError) copy() *NestedError {
var inner *NestedError var inner *NestedError
if ef.inner != nil { if ne.inner != nil {
inner = ef.inner.copy() inner = ne.inner.copy()
} }
return &NestedError{ return &NestedError{
subject: ef.subject, subject: ne.subject,
message: ef.message, message: ne.message,
extras: ef.extras, extras: ne.extras,
inner: inner, inner: inner,
level: ef.level,
} }
} }
func (ef *NestedError) writeIndents(sb *strings.Builder, level int) { func (ne *NestedError) writeIndents(sb *strings.Builder, level int) {
for i := 0; i < level; i++ { for i := 0; i < level; i++ {
sb.WriteString(" ") sb.WriteString(" ")
} }
} }
func (ef *NestedError) writeToSB(sb *strings.Builder, prefix string) { func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
ef.writeIndents(sb, ef.level) ne.writeIndents(sb, level)
sb.WriteString(prefix) sb.WriteString(prefix)
if ef.subject != "" { if ne.subject != "" {
sb.WriteRune('"') sb.WriteString(ne.subject)
sb.WriteString(ef.subject) if ne.message != "" {
sb.WriteRune('"') sb.WriteString(": ")
if ef.message != "" { }
}
if ne.message != "" {
sb.WriteString(ne.message)
}
if ne.HasExtras() || ne.HasInner() {
sb.WriteString(":\n") sb.WriteString(":\n")
} else {
sb.WriteRune('\n')
} }
} level += 1
if ef.message != "" { for _, l := range ne.extras {
ef.writeIndents(sb, ef.level)
sb.WriteString(ef.message)
sb.WriteRune('\n')
}
for _, l := range ef.extras {
l = strings.TrimSpace(l)
if l == "" { if l == "" {
continue continue
} }
ef.writeIndents(sb, ef.level) ne.writeIndents(sb, level)
sb.WriteString("- ") sb.WriteString("- ")
sb.WriteString(l) sb.WriteString(l)
sb.WriteRune('\n') sb.WriteRune('\n')
} }
if ef.inner != nil { if ne.inner != nil {
ef.inner.writeToSB(sb, "- ") ne.inner.writeToSB(sb, level, "- ")
} }
} }

View file

@ -0,0 +1,66 @@
package main
import (
"testing"
)
func AssertEq(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("expected %q, got %q", want, got)
}
}
func TestErrorSimple(t *testing.T) {
ne := NewNestedError("foo bar")
AssertEq(t, ne.Error(), "foo bar")
ne.Subject("baz")
AssertEq(t, ne.Error(), "baz: foo bar")
}
func TestErrorSubjectOnly(t *testing.T) {
ne := NewNestedError("").Subject("bar")
AssertEq(t, ne.Error(), "bar")
}
func TestErrorExtra(t *testing.T) {
ne := NewNestedError("foo").Extra("bar").Extra("baz")
AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n")
}
func TestErrorNested(t *testing.T) {
inner := NewNestedError("inner").
Extra("123").
Extra("456")
inner2 := NewNestedError("inner").
Subject("2").
Extra("456").
Extra("789")
inner3 := NewNestedError("inner").
Subject("3").
Extra("456").
Extra("789")
ne := NewNestedError("foo").
Extra("bar").
Extra("baz").
ExtraError(inner).
With(inner.With(inner2.With(inner3)))
want :=
`foo:
- bar
- baz
- inner:
- 123
- 456
- inner:
- 123
- 456
- 2: inner:
- 456
- 789
- 3: inner:
- 456
- 789
`
AssertEq(t, ne.Error(), want)
}

View file

@ -34,7 +34,7 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
tr = transport tr = transport
} }
proxy := NewSingleHostReverseProxy(url, tr) proxy := NewReverseProxy(url, tr, config)
route := &HTTPRoute{ route := &HTTPRoute{
Alias: config.Alias, Alias: config.Alias,
@ -42,26 +42,24 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
Path: config.Path, Path: config.Path,
Proxy: proxy, Proxy: proxy,
PathMode: config.PathMode, PathMode: config.PathMode,
l: hrlog.WithFields(logrus.Fields{ l: logrus.WithField("alias", config.Alias),
"alias": config.Alias,
// "path": config.Path,
// "path_mode": config.PathMode,
}),
} }
var rewriteBegin = proxy.Rewrite var rewriteBegin = proxy.Rewrite
var rewrite func(*ProxyRequest) var rewrite func(*ProxyRequest)
var modifyResponse func(*http.Response) error var modifyResponse func(*http.Response) error
switch { // no path or forward path
case config.Path == "", config.PathMode == ProxyPathMode_Forward: if config.Path == "" || config.PathMode == ProxyPathMode_Forward {
rewrite = rewriteBegin rewrite = rewriteBegin
case config.PathMode == ProxyPathMode_RemovedPath: } else {
switch config.PathMode {
case ProxyPathMode_RemovedPath:
rewrite = func(pr *ProxyRequest) { rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr) rewriteBegin(pr)
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
} }
case config.PathMode == ProxyPathMode_Sub: case ProxyPathMode_Sub:
rewrite = func(pr *ProxyRequest) { rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr) rewriteBegin(pr)
// disable compression // disable compression
@ -73,6 +71,7 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
default: default:
return nil, NewNestedError("invalid path mode").Subject(config.PathMode) return nil, NewNestedError("invalid path mode").Subject(config.PathMode)
} }
}
if logLevel == logrus.DebugLevel { if logLevel == logrus.DebugLevel {
route.Proxy.Rewrite = func(pr *ProxyRequest) { route.Proxy.Rewrite = func(pr *ProxyRequest) {
@ -96,8 +95,9 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
} }
func (r *HTTPRoute) Start() { func (r *HTTPRoute) Start() {
// dummy httpRoutes.Get(r.Alias).Add(r.Path, r)
} }
func (r *HTTPRoute) Stop() { func (r *HTTPRoute) Stop() {
httpRoutes.Delete(r.Alias) httpRoutes.Delete(r.Alias)
} }

View file

@ -2,10 +2,9 @@ package main
import "github.com/sirupsen/logrus" import "github.com/sirupsen/logrus"
var palog = logrus.WithField("component", "panel") var palog = logrus.WithField("?", "panel")
var prlog = logrus.WithField("component", "provider") var cfgl = logrus.WithField("?", "config")
var cfgl = logrus.WithField("component", "config") var hrlog = logrus.WithField("?", "http")
var hrlog = logrus.WithField("component", "http_proxy") var srlog = logrus.WithField("?", "stream")
var srlog = logrus.WithField("component", "stream") var wlog = logrus.WithField("?", "watcher")
var wlog = logrus.WithField("component", "watcher") var aclog = logrus.WithField("?", "autocert")
var aclog = logrus.WithField("component", "autocert")

View file

@ -43,6 +43,8 @@ func main() {
return return
} }
initSchema()
cfg = NewConfig(configPath) cfg = NewConfig(configPath)
cfg.MustLoad() cfg.MustLoad()

View file

@ -1,8 +1,6 @@
package main package main
import ( import (
"sync"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -12,13 +10,15 @@ type Provider struct {
watcher Watcher watcher Watcher
routes map[string]Route // id -> Route routes map[string]Route // id -> Route
mutex sync.Mutex
l logrus.FieldLogger l logrus.FieldLogger
reloadReqCh chan struct{}
} }
// Init is called after LoadProxyConfig // Init is called after LoadProxyConfig
func (p *Provider) Init(name string) error { func (p *Provider) Init(name string) error {
p.l = prlog.WithFields(logrus.Fields{"kind": p.Kind, "name": name}) p.l = logrus.WithField("provider", name)
p.reloadReqCh = make(chan struct{}, 1)
defer p.initWatcher() defer p.initWatcher()
if err := p.loadProxyConfig(); err != nil { if err := p.loadProxyConfig(); err != nil {
@ -40,8 +40,11 @@ func (p *Provider) StopAllRoutes() {
} }
func (p *Provider) ReloadRoutes() { func (p *Provider) ReloadRoutes() {
p.mutex.Lock() select {
defer p.mutex.Unlock() case p.reloadReqCh <- struct{}{}:
defer func() {
<-p.reloadReqCh
}()
p.StopAllRoutes() p.StopAllRoutes()
err := p.loadProxyConfig() err := p.loadProxyConfig()
@ -50,6 +53,10 @@ func (p *Provider) ReloadRoutes() {
return return
} }
p.StartAllRoutes() p.StartAllRoutes()
default:
p.l.Info("reload request already in progress")
return
}
} }
func (p *Provider) loadProxyConfig() error { func (p *Provider) loadProxyConfig() error {
@ -97,9 +104,9 @@ func (p *Provider) initWatcher() error {
if err != nil { if err != nil {
return NewNestedError("unable to create docker client").With(err) return NewNestedError("unable to create docker client").With(err)
} }
p.watcher = NewDockerWatcher(dockerClient, p.ReloadRoutes) p.watcher = p.NewDockerWatcher(dockerClient)
case ProviderKind_File: case ProviderKind_File:
p.watcher = NewFileWatcher(p.GetFilePath(), p.ReloadRoutes, p.StopAllRoutes) p.watcher = p.NewFileWatcher()
} }
return nil return nil
} }

View file

@ -1,6 +1,9 @@
package main package main
import "fmt" import (
"fmt"
"net/http"
)
type ProxyConfig struct { type ProxyConfig struct {
Alias string `yaml:"-" json:"-"` Alias string `yaml:"-" json:"-"`
@ -11,19 +14,13 @@ type ProxyConfig struct {
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
Path string `yaml:"path" json:"path"` // http proxy only Path string `yaml:"path" json:"path"` // http proxy only
PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
provider *Provider HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
} }
type ProxyConfigMap map[string]ProxyConfig type ProxyConfigMap map[string]ProxyConfig
type ProxyConfigSlice []ProxyConfig type ProxyConfigSlice []ProxyConfig
func NewProxyConfig(provider *Provider) ProxyConfig {
return ProxyConfig{
provider: provider,
}
}
// used by `GetFileProxyConfigs` // used by `GetFileProxyConfigs`
func (cfg *ProxyConfig) SetDefaults() error { func (cfg *ProxyConfig) SetDefaults() error {
err := NewNestedError("invalid proxy config").Subject(cfg.Alias) err := NewNestedError("invalid proxy config").Subject(cfg.Alias)

View file

@ -0,0 +1,92 @@
package main
import (
"errors"
"fmt"
"net/http"
"strings"
)
type ProxyLabel struct {
Alias string
Field string
Value any
}
var errNotProxyLabel = errors.New("not a proxy label")
var errInvalidSetHeaderLine = errors.New("invalid set header line")
var errInvalidBoolean = errors.New("invalid boolean")
const proxyLabelNamespace = "proxy"
func parseProxyLabel(label string, value string) (*ProxyLabel, error) {
ns := strings.Split(label, ".")
var v any = value
if len(ns) != 3 {
return nil, errNotProxyLabel
}
if ns[0] != proxyLabelNamespace {
return nil, errNotProxyLabel
}
field := ns[2]
var err error
parser, ok := valueParser[field]
if ok {
v, err = parser(v.(string))
if err != nil {
return nil, err
}
}
return &ProxyLabel{
Alias: ns[1],
Field: field,
Value: v,
}, nil
}
func setHeadersParser(value string) (any, error) {
value = strings.TrimSpace(value)
lines := strings.Split(value, "\n")
h := make(http.Header)
for _, line := range lines {
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("%w: %q", errInvalidSetHeaderLine, line)
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
h.Add(key, val)
}
return h, nil
}
func commaSepParser(value string) (any, error) {
v := strings.Split(value, ",")
for i := range v {
v[i] = strings.TrimSpace(v[i])
}
return v, nil
}
func boolParser(value string) (any, error) {
switch strings.ToLower(value) {
case "true", "yes", "1":
return true, nil
case "false", "no", "0":
return false, nil
default:
return nil, fmt.Errorf("%w: %q", errInvalidBoolean, value)
}
}
var valueParser = map[string]func(string) (any, error){
"set_headers": setHeadersParser,
"hide_headers": commaSepParser,
"no_tls_verify": boolParser,
}

View file

@ -0,0 +1,186 @@
package main
import (
"errors"
"fmt"
"net/http"
"reflect"
"testing"
)
func makeLabel(alias string, field string) string {
return fmt.Sprintf("proxy.%s.%s", alias, field)
}
func TestNotProxyLabel(t *testing.T) {
pl, err := parseProxyLabel("foo.bar", "1234")
if !errors.Is(err, errNotProxyLabel) {
t.Errorf("expected err NotProxyLabel, got %v", err)
}
if pl != nil {
t.Errorf("expected nil, got %v", pl)
}
_, err = parseProxyLabel("proxy.foo", "bar")
if !errors.Is(err, errNotProxyLabel) {
t.Errorf("expected err InvalidProxyLabel, got %v", err)
}
}
func TestStringProxyLabel(t *testing.T) {
alias := "foo"
field := "ip"
v := "bar"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
if pl.Value != v {
t.Errorf("expected value=%q, got %s", v, pl.Value)
}
}
func TestBoolProxyLabelValid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
tests := map[string]bool{
"true": true,
"TRUE": true,
"yes": true,
"1": true,
"false": false,
"FALSE": false,
"no": false,
"0": false,
}
for k, v := range tests {
pl, err := parseProxyLabel(makeLabel(alias, field), k)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
if pl.Value != v {
t.Errorf("expected value=%v, got %v", v, pl.Value)
}
}
}
func TestBoolProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
_, err := parseProxyLabel(makeLabel(alias, field), "invalid")
if !errors.Is(err, errInvalidBoolean) {
t.Errorf("expected err InvalidProxyLabel, got %v", err)
}
}
func TestHeaderProxyLabelValid(t *testing.T) {
alias := "foo"
field := "set_headers"
v := `
X-Custom-Header1: foo
X-Custom-Header1: bar
X-Custom-Header2: baz
`
h := make(http.Header, 0)
h.Set("X-Custom-Header1", "foo")
h.Add("X-Custom-Header1", "bar")
h.Set("X-Custom-Header2", "baz")
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
hGot, ok := pl.Value.(http.Header)
if !ok {
t.Error("value is not http.Header")
return
}
for k, vWant := range h {
vGot := hGot[k]
if !reflect.DeepEqual(vGot, vWant) {
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
}
}
}
func TestHeaderProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "set_headers"
tests := []string{
"X-Custom-Header1 = bar",
"X-Custom-Header1",
}
for _, v := range tests {
_, err := parseProxyLabel(makeLabel(alias, field), v)
if !errors.Is(err, errInvalidSetHeaderLine) {
t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err)
}
}
}
func TestCommaSepProxyLabelSingle(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}
func TestCommaSepProxyLabelMulti(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}

View file

@ -1,6 +1,6 @@
package main package main
// A small mod on net/http/httputils // A small mod on net/http/httputil/reverseproxy.go
// that doubled the performance // that doubled the performance
import ( import (
@ -8,14 +8,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
"net/http/httptrace" "net/http/httptrace"
"net/textproto" "net/textproto"
"net/url" "net/url"
"strings" "strings"
"time"
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
) )
@ -39,16 +37,16 @@ type ProxyRequest struct {
// //
// SetURL rewrites the outbound Host header to match the target's host. // SetURL rewrites the outbound Host header to match the target's host.
// To preserve the inbound request's Host header (the default behavior // To preserve the inbound request's Host header (the default behavior
// of [NewSingleHostReverseProxy]): // of [NewReverseProxy]):
// //
// rewriteFunc := func(r *httputil.ProxyRequest) { // rewriteFunc := func(r *httputil.ProxyRequest) {
// r.SetURL(url) // r.SetURL(url)
// r.Out.Host = r.In.Host // r.Out.Host = r.In.Host
// } // }
func (r *ProxyRequest) SetURL(target *url.URL) { // func (r *ProxyRequest) SetURL(target *url.URL) {
rewriteRequestURL(r.Out, target) // rewriteRequestURL(r.Out, target)
r.Out.Host = "" // r.Out.Host = ""
} // }
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and // SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
// X-Forwarded-Proto headers of the outbound request. // X-Forwarded-Proto headers of the outbound request.
@ -132,17 +130,17 @@ type ReverseProxy struct {
// recognizes a response as a streaming response, or // recognizes a response as a streaming response, or
// if its ContentLength is -1; for such responses, writes // if its ContentLength is -1; for such responses, writes
// are flushed to the client immediately. // are flushed to the client immediately.
FlushInterval time.Duration // FlushInterval time.Duration
// ErrorLog specifies an optional logger for errors // ErrorLog specifies an optional logger for errors
// that occur when attempting to proxy the request. // that occur when attempting to proxy the request.
// If nil, logging is done via the log package's standard logger. // If nil, logging is done via the log package's standard logger.
ErrorLog *log.Logger // ErrorLog *log.Logger
// BufferPool optionally specifies a buffer pool to // BufferPool optionally specifies a buffer pool to
// get byte slices for use by io.CopyBuffer when // get byte slices for use by io.CopyBuffer when
// copying HTTP response bodies. // copying HTTP response bodies.
BufferPool BufferPool // BufferPool BufferPool
// ModifyResponse is an optional function that modifies the // ModifyResponse is an optional function that modifies the
// Response from the backend. It is called if the backend // Response from the backend. It is called if the backend
@ -203,18 +201,18 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
return a.Path + b.Path, apath + bpath return a.Path + b.Path, apath + bpath
} }
// NewSingleHostReverseProxy returns a new [ReverseProxy] that routes // NewReverseProxy returns a new [ReverseProxy] that routes
// URLs to the scheme, host, and base path provided in target. If the // URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir", // target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir. // the target request will be for /base/dir.
// //
// NewSingleHostReverseProxy does not rewrite the Host header. // NewReverseProxy does not rewrite the Host header.
// //
// To customize the ReverseProxy behavior beyond what // To customize the ReverseProxy behavior beyond what
// NewSingleHostReverseProxy provides, use ReverseProxy directly // NewReverseProxy provides, use ReverseProxy directly
// with a Rewrite function. The ProxyRequest SetURL method // with a Rewrite function. The ProxyRequest SetURL method
// may be used to route the outbound request. (Note that SetURL, // may be used to route the outbound request. (Note that SetURL,
// unlike NewSingleHostReverseProxy, rewrites the Host header // unlike NewReverseProxy, rewrites the Host header
// of the outbound request by default.) // of the outbound request by default.)
// //
// proxy := &ReverseProxy{ // proxy := &ReverseProxy{
@ -223,9 +221,34 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
// r.Out.Host = r.In.Host // if desired // r.Out.Host = r.In.Host // if desired
// }, // },
// } // }
func NewSingleHostReverseProxy(target *url.URL, transport *http.Transport) *ReverseProxy { func NewReverseProxy(target *url.URL, transport *http.Transport, config *ProxyConfig) *ReverseProxy {
// check on init rather than on request
var setHeaders = func(r *http.Request) {}
var hideHeaders = func(r *http.Request) {}
if len(config.SetHeaders) > 0 {
setHeaders = func(r *http.Request) {
h := config.SetHeaders.Clone()
for k, vv := range h {
if k == "Host" {
r.Host = vv[0]
} else {
r.Header[k] = vv
}
}
}
}
if len(config.HideHeaders) > 0 {
hideHeaders = func(r *http.Request) {
for _, k := range config.HideHeaders {
r.Header.Del(k)
}
}
}
return &ReverseProxy{Rewrite: func(pr *ProxyRequest) { return &ReverseProxy{Rewrite: func(pr *ProxyRequest) {
rewriteRequestURL(pr.Out, target) rewriteRequestURL(pr.Out, target)
pr.SetXForwarded()
setHeaders(pr.Out)
hideHeaders(pr.Out)
}, Transport: transport} }, Transport: transport}
} }
@ -380,7 +403,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Strip client-provided forwarding headers. // Strip client-provided forwarding headers.
// The Rewrite func may use SetXForwarded to set new values // The Rewrite func may use SetXForwarded to set new values
// for these or copy the previous values from the inbound request. // for these or copy the previous values from the inbound request.
// outreq.Header.Del("Forwarded") outreq.Header.Del("Forwarded")
// outreq.Header.Del("X-Forwarded-For") // outreq.Header.Del("X-Forwarded-For")
// outreq.Header.Del("X-Forwarded-Host") // outreq.Header.Del("X-Forwarded-Host")
// outreq.Header.Del("X-Forwarded-Proto") // outreq.Header.Del("X-Forwarded-Proto")
@ -388,12 +411,10 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// NOTE: removed // NOTE: removed
// Remove unparsable query parameters from the outbound request. // Remove unparsable query parameters from the outbound request.
// outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery) // outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
pr := &ProxyRequest{ pr := &ProxyRequest{
In: req, In: req,
Out: outreq, Out: outreq,
} }
pr.SetXForwarded() // NOTE: added
p.Rewrite(pr) p.Rewrite(pr)
outreq = pr.Out outreq = pr.Out
// NOTE: removed // NOTE: removed
@ -637,11 +658,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// } // }
func (p *ReverseProxy) logf(format string, args ...any) { func (p *ReverseProxy) logf(format string, args ...any) {
if p.ErrorLog != nil { // if p.ErrorLog != nil {
p.ErrorLog.Printf(format, args...) // p.ErrorLog.Printf(format, args...)
} else { // } else {
hrlog.Printf(format, args...) hrlog.Errorf(format, args...)
} // }
} }
// NOTE: removed // NOTE: removed

View file

@ -0,0 +1,102 @@
package main
import (
"net/http"
"net/url"
"os"
"reflect"
"testing"
"time"
)
var proxyCfg ProxyConfig
var proxyUrl, _ = url.Parse("http://127.0.0.1:8181")
var proxyServer = NewServer(ServerOptions{
Name: "proxy",
HTTPAddr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
NewReverseProxy(proxyUrl, &http.Transport{}, &proxyCfg).ServeHTTP(w, r)
}),
})
var testServer = NewServer(ServerOptions{
Name: "test",
HTTPAddr: ":8181",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header
for k, vv := range h {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(http.StatusOK)
}),
})
var httpClient = http.DefaultClient
func TestMain(m *testing.M) {
proxyServer.Start()
testServer.Start()
time.Sleep(100 * time.Millisecond)
code := m.Run()
proxyServer.Stop()
testServer.Stop()
os.Exit(code)
}
func TestSetHeader(t *testing.T) {
hWant := http.Header{"X-Test": []string{"foo", "bar"}, "X-Test2": []string{"baz"}}
proxyCfg = ProxyConfig{
Alias: "test",
Scheme: "http",
Host: "127.0.0.1",
Port: "8181",
SetHeaders: hWant,
}
req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
hGot := resp.Header
t.Log("headers: ", hGot)
for k, v := range hWant {
if !reflect.DeepEqual(hGot[k], v) {
t.Errorf("header %s: expected %v, got %v", k, v, hGot[k])
}
}
}
func TestHideHeader(t *testing.T) {
hHide := []string{"X-Test", "X-Test2"}
proxyCfg = ProxyConfig{
Alias: "test",
Scheme: "http",
Host: "127.0.0.1",
Port: "8181",
HideHeaders: hHide,
}
req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
for _, k := range hHide {
req.Header.Set(k, "foo")
}
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
hGot := resp.Header
t.Log("headers: ", hGot)
for _, v := range hHide {
_, ok := hGot[v]
if ok {
t.Errorf("header %s: expected hidden, got %v", v, hGot[v])
}
}
}

View file

@ -22,7 +22,6 @@ func NewRoute(cfg *ProxyConfig) (Route, error) {
if err != nil { if err != nil {
return nil, NewNestedErrorFrom(err).Subject(cfg.Alias) return nil, NewNestedErrorFrom(err).Subject(cfg.Alias)
} }
httpRoutes.Get(cfg.Alias).Add(cfg.Path, route)
return route, nil return route, nil
} }
} }

View file

@ -48,10 +48,18 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
var srcPort, dstPort string var srcPort, dstPort string
var srcScheme, dstScheme string var srcScheme, dstScheme string
l := srlog.WithFields(logrus.Fields{
"alias": config.Alias,
})
portSplit := strings.Split(config.Port, ":") portSplit := strings.Split(config.Port, ":")
if len(portSplit) != 2 { if len(portSplit) != 2 {
cfgl.Warnf("invalid port %s, assuming it is target port", config.Port) l.Warnf(
srcPort = "0" `%s: invalid port %s,
assuming it is target port`,
config.Alias,
config.Port,
)
srcPort = "0" // will assign later
dstPort = config.Port dstPort = config.Port
} else { } else {
srcPort = portSplit[0] srcPort = portSplit[0]
@ -101,11 +109,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
stopCh: make(chan struct{}, 1), stopCh: make(chan struct{}, 1),
connCh: make(chan interface{}), connCh: make(chan interface{}),
started: false, started: false,
l: srlog.WithFields(logrus.Fields{ l: l,
"alias": config.Alias,
// "src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt),
// "dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt),
}),
}, nil }, nil
} }

View file

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -212,13 +213,17 @@ func setFieldFromSnake[T interface{}, VT interface{}](obj *T, field string, valu
field = utils.snakeToPascal(field) field = utils.snakeToPascal(field)
prop := reflect.ValueOf(obj).Elem().FieldByName(field) prop := reflect.ValueOf(obj).Elem().FieldByName(field)
if prop.Kind() == 0 { if prop.Kind() == 0 {
return NewNestedError("unknown field").Subject(field) return errors.New("unknown field")
} }
prop.Set(reflect.ValueOf(value)) prop.Set(reflect.ValueOf(value))
return nil return nil
} }
func validateYaml(schema *jsonschema.Schema, data []byte) error { func validateYaml(schema *jsonschema.Schema, data []byte) error {
if noSchemaValidation {
return nil
}
var i interface{} var i interface{}
err := yaml.Unmarshal(data, &i) err := yaml.Unmarshal(data, &i)

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"path" "strings"
"sync" "sync"
"time" "time"
@ -22,8 +22,6 @@ type Watcher interface {
} }
type watcherBase struct { type watcherBase struct {
name string // for log / error output
kind string // for log / error output
onChange func() onChange func()
l logrus.FieldLogger l logrus.FieldLogger
sync.Mutex sync.Mutex
@ -42,30 +40,44 @@ type dockerWatcher struct {
wg sync.WaitGroup wg sync.WaitGroup
} }
func newWatcher(kind string, name string, onChange func()) *watcherBase { func (p *Provider) newWatcher() *watcherBase {
return &watcherBase{ return &watcherBase{
kind: kind, onChange: p.ReloadRoutes,
name: name, l: p.l,
onChange: onChange,
l: wlog.WithFields(logrus.Fields{"kind": kind, "name": name}),
}
}
func NewFileWatcher(p string, onChange func(), onDelete func()) Watcher {
return &fileWatcher{
watcherBase: newWatcher("File", path.Base(p), onChange),
path: p,
onDelete: onDelete,
} }
} }
func NewDockerWatcher(c *client.Client, onChange func()) Watcher { func (p *Provider) NewFileWatcher() Watcher {
return &fileWatcher{
watcherBase: p.newWatcher(),
path: p.GetFilePath(),
onDelete: p.StopAllRoutes,
}
}
func (p *Provider) NewDockerWatcher(c *client.Client) Watcher {
return &dockerWatcher{ return &dockerWatcher{
watcherBase: newWatcher("Docker", c.DaemonHost(), onChange), watcherBase: p.newWatcher(),
client: c, client: c,
stopCh: make(chan struct{}, 1), stopCh: make(chan struct{}, 1),
} }
} }
func (c *config) newWatcher() *watcherBase {
return &watcherBase{
onChange: c.MustReload,
l: c.l,
}
}
func (c *config) NewFileWatcher() Watcher {
return &fileWatcher{
watcherBase: c.newWatcher(),
path: c.reader.(*FileReader).Path,
onDelete: func() { c.l.Fatal("config file deleted") },
}
}
func (w *fileWatcher) Start() { func (w *fileWatcher) Start() {
w.Lock() w.Lock()
defer w.Unlock() defer w.Unlock()
@ -100,7 +112,7 @@ func (w *fileWatcher) Dispose() {
func (w *dockerWatcher) Start() { func (w *dockerWatcher) Start() {
w.Lock() w.Lock()
defer w.Unlock() defer w.Unlock()
dockerWatchMap.Set(w.name, w) dockerWatchMap.Set(w.client.DaemonHost(), w)
w.wg.Add(1) w.wg.Add(1)
go w.watch() go w.watch()
} }
@ -114,7 +126,7 @@ func (w *dockerWatcher) Stop() {
close(w.stopCh) close(w.stopCh)
w.wg.Wait() w.wg.Wait()
w.stopCh = nil w.stopCh = nil
dockerWatchMap.Delete(w.name) dockerWatchMap.Delete(w.client.DaemonHost())
} }
func (w *dockerWatcher) Dispose() { func (w *dockerWatcher) Dispose() {
@ -164,10 +176,10 @@ func watchFiles() {
} }
switch { switch {
case event.Has(fsnotify.Write): case event.Has(fsnotify.Write):
w.l.Info("file changed") w.l.Info("file changed: ", event.Name)
go w.onChange() go w.onChange()
case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename): case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename):
w.l.Info("file renamed / deleted") w.l.Info("file renamed / deleted: ", event.Name)
go w.onDelete() go w.onDelete()
} }
case err := <-fsWatcher.Errors: case err := <-fsWatcher.Errors:
@ -194,16 +206,20 @@ func (w *dockerWatcher) watch() {
case <-w.stopCh: case <-w.stopCh:
return return
case msg := <-msgChan: case msg := <-msgChan:
w.l.Infof("container %s %s", msg.Actor.Attributes["name"], msg.Action) containerName := msg.Actor.Attributes["name"]
if strings.HasPrefix(containerName, "buildx_buildkit_builder-") {
continue
}
w.l.Infof("container %s %s", containerName, msg.Action)
go w.onChange() go w.onChange()
case err := <-errChan: case err := <-errChan:
switch { switch {
case client.IsErrConnectionFailed(err): case client.IsErrConnectionFailed(err):
w.l.Error(NewNestedError("connection failed").Subject(w.name)) w.l.Error("watcher: connection failed")
case client.IsErrNotFound(err): case client.IsErrNotFound(err):
w.l.Error(NewNestedError("endpoint not found").Subject(w.name)) w.l.Error("watcher: endpoint not found")
default: default:
w.l.Error(NewNestedErrorFrom(err).Subject(w.name)) w.l.Errorf("watcher: %v", err)
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
msgChan, errChan = listen() msgChan, errChan = listen()

View file

@ -1 +1 @@
0.4.7 0.4.8