Compare commits

...

39 Commits
0.4.1 ... 0.4.6

Author SHA1 Message Date
yusing
03bf425a38 fix selecting wrong port on remote docker provide 2024-03-31 21:33:09 +00:00
yusing
5fafa619ee version bump and binary doc fix 2024-03-31 16:28:48 +00:00
yusing
bebf99ed6c docker example update' 2024-03-31 11:45:23 +00:00
yusing
8483263d01 readme update 2024-03-31 11:32:16 +00:00
yusing
351bf84559 tcp/udp fix 2024-03-31 11:26:39 +00:00
yusing
cbe23d2ed1 tcp/udp fix 2024-03-31 07:04:08 +00:00
yusing
6e45f3683c docs fix 2024-03-30 00:39:05 +00:00
yusing
581894c05b binary setup script fix 2024-03-30 00:15:05 +00:00
yusing
2657b1f726 binary setup script fix 2024-03-29 23:07:17 +00:00
yusing
3505e8ff7e systemd service param update 2024-03-29 23:03:56 +00:00
yusing
2314e39291 workflow update 2024-03-29 22:53:20 +00:00
yusing
bd19f443d4 Merge branch 'main' of github.com:yusing/go-proxy 2024-03-29 22:43:13 +00:00
yusing
ce433f0c51 script update for auto version discovery, dockerfile fix for CI 2024-03-29 22:40:11 +00:00
Yuzerion
47877e5119 Merge pull request #12 from yusing/dev
0.4.4
2024-03-30 06:07:34 +08:00
yusing
486122f3d8 no timestamp, color and sorting in systemd mode 2024-03-29 21:45:29 +00:00
yusing
a0be1f11d3 script systemd auto restart on crash 2024-03-29 21:43:43 +00:00
yusing
662190e09e scripts fix 2024-03-29 21:29:06 +00:00
yusing
ce1e5da72e scripts fix 2024-03-29 21:22:51 +00:00
yusing
eb7e744a75 scripts fix 2024-03-29 21:20:18 +00:00
yusing
ac26baf97f scripts fix 2024-03-29 21:12:38 +00:00
yusing
5a8c11de16 docs update, added setup scripts 2024-03-29 21:02:21 +00:00
yusing
a8ecafcd09 workflow updte 2024-03-29 19:21:05 +00:00
Yuzerion
af37d1f29e Merge pull request #10 from yusing/test-go-workflow
Update go.yml
2024-03-30 03:06:16 +08:00
Yuzerion
8cfd24e6bd Update go.yml 2024-03-30 00:09:08 +08:00
Yuzerion
7bf5784016 Merge pull request #9 from yusing/test-go-workflow
Create go.yml
2024-03-30 00:01:49 +08:00
Yuzerion
25930a1a73 Create go.yml 2024-03-30 00:00:35 +08:00
Yuzerion
f20a1ff523 Merge pull request #7 from yusing/test-docker-image
Create docker-image.yml
2024-03-29 23:58:03 +08:00
Yuzerion
ba51796a64 Create docker-image.yml 2024-03-29 23:56:55 +08:00
yusing
c445d50221 smarter port selection 2024-03-29 13:55:28 +00:00
yusing
73dfc17a82 smarter port selection 2024-03-29 13:35:10 +00:00
yusing
fdab026a3b fix docker port discovery 2024-03-29 13:20:44 +00:00
yusing
c789c69c86 codemirror 5 fix for config edit 2024-03-29 13:13:26 +00:00
Yuzerion
2b298aa7fa Update README.md 2024-03-29 21:01:18 +08:00
yusing
d20e4d435a verify -> validate 2024-03-29 01:50:00 +00:00
yusing
15d9436d52 readme update 2024-03-29 01:47:13 +00:00
yusing
ca98b31458 fix default config value 2024-03-29 01:38:58 +00:00
yusing
77f957c7a8 makefile update 2024-03-29 01:31:51 +00:00
yusing
51493c9fdd makefile update 2024-03-29 01:28:44 +00:00
yusing
9b34dc994d added new file button in config editor, dockerfile fix 2024-03-29 01:24:47 +00:00
44 changed files with 997 additions and 488 deletions

14
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Docker Image CI
on:
push:
tags:
- "*"
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Build and Push Container to ghcr.io
uses: GlueOps/github-actions-build-push-containers@v0.3.7
with:
tags: latest,${{ github.ref_name }}

30
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22.1"
- name: Build
run: make build
- name: Release
uses: softprops/action-gh-release@v2
with:
files: bin/go-proxy
#- name: Test
# run: go test -v ./...

11
.gitignore vendored
View File

@@ -1,10 +1,9 @@
compose.yml
config/**
bin/go-proxy.bak
config/
certs/
bin/
templates/codemirror/
logs/
log/
config-editor/
log/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "templates/codemirror"]
path = templates/codemirror
url = https://github.com/codemirror/codemirror5.git

12
.vscode/settings.json vendored
View File

@@ -1,14 +1,16 @@
{
"go.inferGopath": false,
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"config.example.yml",
"config.yml",
"file:///config/workspace/go-proxy/config.example.yml"
],
// "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

@@ -1,17 +1,34 @@
FROM alpine:latest AS codemirror
RUN apk add --no-cache unzip wget make
COPY Makefile .
RUN make setup-codemirror
FROM golang:1.22.1-alpine as builder
COPY src/ /src
COPY go.mod go.sum /src/go-proxy
WORKDIR /src/go-proxy
RUN --mount=type=cache,target="/go/pkg/mod" \
go mod download
ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="/go/pkg/mod" \
--mount=type=cache,target="/root/.cache/go-build" \
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy
FROM alpine:latest
LABEL maintainer="yusing@6uo.me"
RUN apk add --no-cache bash tzdata
RUN mkdir /app
COPY bin/go-proxy entrypoint.sh /app/
RUN apk add --no-cache tzdata
RUN mkdir -p /app/templates
COPY --from=codemirror templates/codemirror/ /app/templates/codemirror
COPY templates/ /app/templates
COPY config.example.yml /app/config/config.yml
COPY schema/ /app/schema
COPY --from=builder /src/go-proxy /app/
RUN chmod +x /app/go-proxy /app/entrypoint.sh
RUN chmod +x /app/go-proxy
ENV DOCKER_HOST unix:///var/run/docker.sock
ENV GOPROXY_DEBUG 0
ENV GOPROXY_REDIRECT_HTTP 1
EXPOSE 80
EXPOSE 8080
@@ -19,4 +36,4 @@ EXPOSE 443
EXPOSE 8443
WORKDIR /app
ENTRYPOINT /app/entrypoint.sh
CMD ["/app/go-proxy"]

View File

@@ -2,22 +2,28 @@
all: build quick-restart logs
setup:
mkdir -p config certs
[ -f config/config.yml ] || cp config.example.yml config/config.yml
[ -f config/providers.yml ] || touch config/providers.yml
setup-codemirror:
wget https://codemirror.net/5/codemirror.zip
unzip codemirror.zip
rm codemirror.zip
mkdir -p templates
mv codemirror-* templates/codemirror
build:
mkdir -p bin
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
up:
docker compose up -d --build go-proxy
quick-restart: # quick restart without restarting the container
docker cp bin/go-proxy go-proxy:/app/go-proxy
docker cp templates/* go-proxy:/app/templates
docker cp entrypoint.sh go-proxy:/app/entrypoint.sh
docker exec -d go-proxy bash /app/entrypoint.sh restart
docker compose up -d --build app
restart:
docker kill go-proxy
docker compose up -d go-proxy
docker compose up -d app
logs:
tail -f log/go-proxy.log
@@ -30,6 +36,6 @@ udp-server:
-p 9999:9999/udp \
--label proxy.test-udp.scheme=udp \
--label proxy.test-udp.port=20003:9999 \
--network data_default \
--network host \
--name test-udp \
$$(docker build -q -f udp-test-server.Dockerfile .)

160
README.md
View File

@@ -10,8 +10,8 @@ In the examples domain `x.y.z` is used, replace them with your domain
- [Table of content](#table-of-content)
- [Key Points](#key-points)
- [How to use](#how-to-use)
- [Binary](#binary)
- [Docker](#docker)
- [Command-line args](#command-line-args)
- [Commands](#commands)
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
- [Configuration](#configuration)
- [Labels (docker)](#labels-docker)
@@ -37,78 +37,50 @@ In the examples domain `x.y.z` is used, replace them with your domain
- Fast (See [benchmarks](#benchmarks))
- Auto certificate obtaining and renewal (See [Config File](#config-file) and [Supported DNS Challenge Providers](#supported-dns-challenge-providers))
- Auto detect reverse proxies from docker
- Auto hot-reload on container `start` / `die` / `stop` or config file changes
- Custom proxy entries with `config.yml` and additional provider files
- Subdomain matching + Path matching **(domain name doesn't matter)**
- HTTP(s) proxy + TCP/UDP Proxy
- HTTP(s) proxy + TCP/UDP Proxy (UDP is _experimental_)
- HTTP(s) round robin load balance support (same subdomain and path across different hosts)
- Auto hot-reload on container `start` / `die` / `stop` or config file changes
- Simple panel to see all reverse proxies and health available on port [panel_port_http] (http) and port [panel_port_https] (https)
- Web UI on port 8080 (http) and port 8443 (https)
![panel screenshot](screenshots/panel.png)
- Config editor to edit config and provider files with validation
- a simple panel to see all reverse proxies and health
**Validate and save file with Ctrl+S**
![panel screenshot](screenshots/panel.png)
![config editor screenshot](screenshots/config_editor.png)
- a config editor to edit config and provider files with validation
**Validate and save file with Ctrl+S**
![config editor screenshot](screenshots/config_editor.png)
## How to use
1. Download and extract the latest release (or clone the repository if you want to try out experimental features)
1. Setup DNS Records to your machine's IP address
2. Copy `config.example.yml` to `config/config.yml` and modify the content to fit your needs
- A Record: `*.y.z` -> `10.0.10.1`
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
3. (Optional) write your own `config/providers.yml` from `providers.example.yml`
2. Start `go-proxy` (see [Binary](docs/binary.md) or [docker](docs/docker.md))
4. See [Binary](#binary) or [docker](#docker)
3. Start editing config files
- with text editor (i.e. Visual Studio Code)
- or with web config editor by navigate to `ip:8080`
### Binary
## Command-line args
1. (Optional) enabled HTTPS
`go-proxy [command]`
- Use autocert feature by completing `autocert` in `config.yml`
### Commands
- Use existing certificate
- empty: start proxy server
- validate: validate config and exit
- reload: trigger a force reload of config
Prepare your wildcard (`*.y.z`) SSL cert in `certs/`
Examples:
- cert / chain / fullchain: `certs/cert.crt`
- private key: `certs/priv.key`
2. run the binary `bin/go-proxy`
3. enjoy
### Docker
1. Copy content from [compose.example.yml](compose.example.yml) and create your own `compose.yml`
2. Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.host` is reachable
3. (Optional) enable HTTPS
- Use autocert feature by completing `autocert` section in `config/config.yml` and mount `certs/` to `/app/certs` in order to store obtained certs
- Use existing certificate by mount your wildcard (`*.y.z`) SSL cert
- cert / chain / fullchain -> `/app/certs/cert.crt`
- private key -> `/app/certs/priv.key`
4. Start `go-proxy` with `docker compose up -d` or `make up`.
5. (Optional) If you are using ufw with vpn that drop all inbound traffic except vpn, run below to allow docker containers to connect to `go-proxy`
In case the network of your container is in subnet `172.16.0.0/16` (bridge),
and vpn network is under `100.64.0.0/10` (i.e. tailscale)
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
You can also list CIDRs of all docker bridge networks by:
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
6. start your docker app, and visit <container_name>.y.z
7. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies
- Binary: `go-proxy reload`
- Docker: `docker exec -it go-proxy /app/go-proxy reload`
## Use JSON Schema in VSCode
@@ -116,22 +88,22 @@ Modify `.vscode/settings.json` to fit your needs
```json
{
"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",
]
}
"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"
]
}
}
```
## Configuration
With container name, no label needs to be added *(most of the time)*.
With container name, no label needs to be added _(most of the time)_.
### Labels (docker)
@@ -143,7 +115,7 @@ See [compose.example.yml](compose.example.yml) for more
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>`** prefix (i.e. `proxy.nginx.scheme: http`)
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
@@ -179,7 +151,6 @@ Below labels has a **`proxy.<alias>`** prefix (i.e. `proxy.nginx.scheme: http`)
### Environment variables
- `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.)
- `GOPROXY_REDIRECT_HTTP`: set to `0` or `false` to disable http to https redirect (only when certs are located)
### Config File
@@ -204,7 +175,7 @@ See [config.example.yml](config.example.yml) for more
values:
- `FROM_ENV`: value from environment
- `FROM_ENV`: value from environment (`DOCKER_HOST`)
- full url to docker host (i.e. `tcp://host:2375`)
- `file`: load reverse proxies from provider file
@@ -225,48 +196,12 @@ 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
To add more provider support (**CloudDNS** as an example):
1. Fork this repo, modify [autocert.go](src/go-proxy/autocert.go#L305)
```go
var providersGenMap = map[string]ProviderGenerator{
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
// add here, i.e.
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
}
```
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
3. Build `go-proxy` with `make build`
4. Set required config in `config.yml` `autocert` -> `options` section
```shell
# From https://go-acme.github.io/lego/dns/clouddns/
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
CLOUDDNS_EMAIL=you@example.com \
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
lego --email you@example.com --dns clouddns --domains my.example.org run
```
Should turn into:
```yaml
autocert:
...
options:
client_id: bLsdFAks23429841238feb177a572aX
email: you@example.com
password: b9841238feb177a84330f
```
5. Run and test if it works
6. Commit and create pull request
To add more provider support, see [this](docs/add_dns_provider.md)
## Examples
See [docker.md](docs/docker.md#docker-compose-example) for complete examples
### Single port configuration example
```yaml
@@ -323,7 +258,7 @@ go-proxy:
ports:
- 80:80
...
- 20000:20000/tcp
- <your desired port>:20000/tcp
# or 20000-20010:20000-20010/tcp to declare large range at once
# access app-db via <*>.y.z:20000
@@ -454,7 +389,7 @@ Local benchmark (client running wrk and `go-proxy` server are under same proxmox
## Known issues
None
UDP proxy does not work for PalWorld Dedicated Server
## Memory usage
@@ -471,6 +406,3 @@ It takes ~15 MB for 50 proxy entries
4. build binary with `make build`
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
[panel_port_http]: 8080
[panel_port_https]: 8443

Binary file not shown.

View File

@@ -1,41 +1,40 @@
version: '3'
services:
app:
build: .
image: ghcr.io/yusing/go-proxy:latest
container_name: go-proxy
restart: always
networks: # ^also add here
- default
# environment:
# - GOPROXY_DEBUG=1 # (optional, enable only for debug)
# - GOPROXY_REDIRECT_HTTP=0 # (optional, uncomment to disable http redirect (http -> https))
ports:
- 80:80 # http
# - 443:443 # optional, https
- 80:80 # http proxy
- 8080:8080 # http panel
# - 443:443 # optional, https proxy
# - 8443:8443 # optional, https panel
# optional, if you declared any tcp/udp proxy, set a range you want to use
# - 20000:20100/tcp
# - 20000:20100/udp
volumes:
- ./config:/app/config
# if local docker provider is used
- /var/run/docker.sock:/var/run/docker.sock:ro
# use existing certificate
# - /path/to/cert.pem:/app/certs/cert.crt:ro
# - /path/to/privkey.pem:/app/certs/priv.key:ro
# use autocert feature
# store autocert obtained cert
# - ./certs:/app/certs
# workaround for "lookup: no such host"
# dns:
# - 127.0.0.1
# if local docker provider is used (by default)
- /var/run/docker.sock:/var/run/docker.sock:ro
# to use custom config and providers
# - ./config:/app/config
dns:
- 127.0.0.1 # workaround for "lookup: no such host"
extra_hosts:
# required if you use local docker provider and have containers in `host` network_mode
- host.docker.internal:host-gateway
# if you have container running in "host" network mode
# extra_hosts:
# - host.docker.internal:host-gateway
logging:
driver: 'json-file'
options:

View File

@@ -1,25 +1,21 @@
# uncomment to use autocert
autocert: # (optional, if you need autocert feature)
email: "user@domain.com" # (required) email for acme certificate
domains: # (required)
- "*.y.z" # domain for acme certificate, use wild card to allow all subdomains
provider: cloudflare # (required) dns challenge provider (string)
options: # provider specific options
auth_token: "YOUR_ZONE_API_TOKEN"
# Autocert (uncomment to enable)
# autocert: # (optional, if you need autocert feature)
# email: "user@domain.com" # (required) email for acme certificate
# domains: # (required)
# - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains
# provider: cloudflare # (required) dns challenge provider (string)
# options: # provider specific options
# auth_token: "YOUR_ZONE_API_TOKEN"
providers:
local:
kind: docker
# for value format, see https://docs.docker.com/reference/cli/dockerd/
# i.e. FROM_ENV, ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375
value: FROM_ENV
# remote1:
# kind: docker
# value: ssh://user@10.0.1.1
# remote2:
# kind: docker
# value: tcp://10.0.1.1:2375
# provider1:
# kind: file
# value: provider1.yml
# provider2:
# kind: file
# value: provider2.yml
providers:
kind: file
value: providers.yml
# Fixed options (optional, non hot-reloadable)
# timeout_shutdown: 5
# redirect_to_https: false

41
docs/add_dns_provider.md Normal file
View File

@@ -0,0 +1,41 @@
# Adding provider support
## **CloudDNS** as an example
1. Fork this repo, modify [autocert.go](../src/go-proxy/autocert.go#L305)
```go
var providersGenMap = map[string]ProviderGenerator{
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
// add here, i.e.
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
}
```
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
3. Build `go-proxy` with `make build`
4. Set required config in `config.yml` `autocert` -> `options` section
```shell
# From https://go-acme.github.io/lego/dns/clouddns/
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
CLOUDDNS_EMAIL=you@example.com \
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
lego --email you@example.com --dns clouddns --domains my.example.org run
```
Should turn into:
```yaml
autocert:
...
options:
client_id: bLsdFAks23429841238feb177a572aX
email: you@example.com
password: b9841238feb177a84330f
```
5. Run and test if it works
6. Commit and create pull request

59
docs/binary.md Normal file
View File

@@ -0,0 +1,59 @@
# Getting started with `go-proxy` (binary)
## Setup
1. Install `bash`, `make` and `wget` if not already
2. Run setup script
To specitfy a version _(optional)_
```shell
export VERSION=latest # will be resolved into real version number
export VERSION=<version>
```
If you don't need web config editor
```shell
export SETUP_CODEMIRROR=0
```
Setup
```shell
wget -qO- https://6uo.me/go-proxy-setup-binary | sudo bash
```
What it does:
- Download source file and binary into /opt/go-proxy/$VERSION
- Setup `config.yml` and `providers.yml`
- Setup `template/codemirror` which is a dependency for web config editor
- Create a systemd service (if available) in `/etc/systemd/system/go-proxy.service`
- Enable and start `go-proxy` service
3. Start editing config files in `http://<ip>:8080`
4. Check logs / status with `systemctl status go-proxy`
## Setup (alternative method)
1. Download the latest release and extract somewhere
2. Run `make setup` and _(optional) `make setup-codemirror`_
3. Enable HTTPS _(optional)_
- To use autocert feature
complete `autocert` in `config/config.yml`
- To use existing certificate
Prepare your wildcard (`*.y.z`) SSL cert in `certs/`
- cert / chain / fullchain: `certs/cert.crt`
- private key: `certs/priv.key`
4. Run the binary `bin/go-proxy`

124
docs/docker.md Normal file
View File

@@ -0,0 +1,124 @@
# Getting started with `go-proxy` docker container
## Setup
1. Install `wget` if not already
2. Run setup script
`bash <(wget -qO- https://6uo.me/go-proxy-setup-docker)`
What it does:
- Create required directories
- Setup `config.yml` and `compose.yml`
3. Verify folder structure and then `cd go-proxy`
```plain
go-proxy
├── certs
├── compose.yml
└── config
├── config.yml
└── providers.yml
```
4. Enable HTTPs _(optional)_
- To use autocert feature
- completing `autocert` section in `config/config.yml`
- mount `certs/` to `/app/certs` to store obtained certs
- To use existing certificate
mount your wildcard (`*.y.z`) SSL cert
- cert / chain / fullchain -> `/app/certs/cert.crt`
- private key -> `/app/certs/priv.key`
5. Modify `compose.yml` fit your needs
Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.host` is reachable
6. Run `docker compose up -d` to start the container
7. Start editing config files in `http://<ip>:8080`
## Troubleshooting
- Firewall issues
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
Explaination:
Docker network is usually `172.16.0.0/16`
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
You can also list CIDRs of all docker bridge networks by:
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
## Docker compose example
```yaml
volumes:
adg-work:
adg-conf:
mc-data:
services:
adg:
image: adguard/adguardhome
restart: unless-stopped
labels:
- proxy.aliases=adg,adg-dns,adg-setup
- proxy.adg.port=80
- proxy.adg-setup.port=3000
- proxy.adg-dns.scheme=udp
- proxy.adg-dns.port=20000:dns
volumes:
- adg-work:/opt/adguardhome/work
- adg-conf:/opt/adguardhome/conf
mc:
image: itzg/minecraft-server
tty: true
stdin_open: true
container_name: mc
restart: unless-stopped
labels:
- proxy.mc.scheme=tcp
- proxy.mc.port=20001:25565
environment:
EULA: "TRUE"
volumes:
- mc-data:/data
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
ports:
- 80:80 # http
- 443:443 # optional, https
- 8080:8080 # http panel
- 8443:8443 # optional, https panel
- 53:20000/udp # adguardhome
- 25565:20001/tcp # minecraft
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.panel.port=8080
```
### Services URLs
- `gp.yourdomain.com`: go-proxy web panel
- `adg-setup.yourdomain.com`: adguard setup (first time running)
- `adg.yourdomain.com`: adguard dashboard
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server

View File

@@ -1,11 +0,0 @@
#!/bin/bash
if [ "$1" == "restart" ]; then
echo "restarting"
killall go-proxy
fi
if [ "$GOPROXY_DEBUG" == "1" ]; then
/app/go-proxy 2> log/go-proxy.log &
tail -f /dev/null
else
/app/go-proxy
fi

9
go.mod
View File

@@ -14,6 +14,7 @@ require (
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/cloudflare-go v0.92.0 // indirect
@@ -27,7 +28,9 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/miekg/dns v1.1.58 // indirect
@@ -39,15 +42,21 @@ require (
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace 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/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

29
go.sum
View File

@@ -1,17 +1,9 @@
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/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
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.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
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.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw=
github.com/cloudflare/cloudflare-go v0.91.0 h1:L7IR+86qrZuEMSjGFg4cwRwtHqC8uCPmMUkP7BD4CPw=
github.com/cloudflare/cloudflare-go v0.91.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
github.com/cloudflare/cloudflare-go v0.92.0 h1:ltJvGvqZ4G6Fm2hHOYZ5RWpJQcrM0oDrsjjZydZhFJQ=
github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -19,8 +11,6 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
@@ -50,9 +40,11 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -68,7 +60,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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -89,7 +80,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/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -98,15 +88,13 @@ 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/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/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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
@@ -132,8 +120,6 @@ golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -147,11 +133,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
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.6.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-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-20210616094352-59db8d763f22/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.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -165,8 +150,6 @@ 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-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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -180,6 +163,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -117,7 +117,17 @@
]
}
}
},
"timeout_shutdown": {
"title": "Shutdown timeout (in seconds)",
"type": "integer",
"minimum": 0
},
"redirect_to_https": {
"title": "Redirect to HTTPS",
"type": "boolean"
}
},
"additionalProperties": false
"additionalProperties": false,
"required": ["providers"]
}

View File

@@ -1,7 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "go-proxy providers file",
"anyOf": [
{
"type":"object"
},
{
"type":"null"
}
],
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"title": "Proxy entry",

114
setup-binary.sh Normal file
View File

@@ -0,0 +1,114 @@
#!/bin/bash
set -e
REPO_URL=https://github.com/yusing/go-proxy
BIN_URL="${REPO_URL}/releases/download/${VERSION}/go-proxy"
SRC_URL="${REPO_URL}/archive/refs/tags/${VERSION}.tar.gz"
APP_ROOT="/opt/go-proxy/${VERSION}"
LOG_FILE="/tmp/go-proxy-setup.log"
if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then
VERSION_URL="${REPO_URL}/raw/main/version.txt"
VERSION=$(wget -qO- "$VERSION_URL")
fi
if [ -d "$APP_ROOT" ]; then
echo "$APP_ROOT already exists"
exit 1
fi
# check if wget exists
if ! [ -x "$(command -v wget)" ]; then
echo "wget is not installed"
exit 1
fi
# check if make exists
if ! [ -x "$(command -v make)" ]; then
echo "make is not installed"
exit 1
fi
dl_source() {
cd /tmp
echo "Downloading go-proxy source ${VERSION}"
wget -c "${SRC_URL}" -O go-proxy.tar.gz &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "Source download failed, check your internet connection and version number"
exit 1
fi
echo "Done"
echo "Extracting go-proxy source ${VERSION}"
tar xzf go-proxy.tar.gz &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "failed to untar go-proxy.tar.gz"
exit 1
fi
rm go-proxy.tar.gz
mkdir -p "$(dirname "${APP_ROOT}")"
mv "go-proxy-${VERSION}" "$APP_ROOT"
cd "$APP_ROOT"
echo "Done"
}
dl_binary() {
mkdir -p bin
echo "Downloading go-proxy binary ${VERSION}"
wget -c "${BIN_URL}" -O bin/go-proxy &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "Binary download failed, check your internet connection and version number"
exit 1
fi
chmod +x bin/go-proxy
echo "Done"
}
setup() {
make setup &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "make setup failed"
exit 1
fi
# SETUP_CODEMIRROR = 1
if [ "$SETUP_CODEMIRROR" != "0" ]; then
make setup-codemirror &> $LOG_FILE || echo "make setup-codemirror failed, ignored"
fi
}
dl_source
dl_binary
setup
# setup systemd
# check if systemctl exists
if ! command -v systemctl is-system-running > /dev/null 2>&1; then
echo "systemctl not found, skipping systemd setup"
exit 0
fi
systemctl_failed() {
echo "Failed to enable and start go-proxy"
systemctl status go-proxy
exit 1
}
echo "Setting up systemd service"
cat <<EOF > /etc/systemd/system/go-proxy.service
[Unit]
Description=go-proxy reverse proxy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Type=simple
ExecStart=${APP_ROOT}/bin/go-proxy
WorkingDirectory=${APP_ROOT}
Environment="IS_SYSTEMD=1"
Restart=on-failure
RestartSec=1s
KillMode=process
KillSignal=SIGINT
TimeoutStartSec=5s
TimeoutStopSec=5s
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload &>$LOG_FILE || systemctl_failed
systemctl enable --now go-proxy &>$LOG_FILE || systemctl_failed
echo "Done"
echo "Setup complete"

14
setup-docker.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -e
if [ -z "$BRANCH" ]; then
BRANCH="main"
fi
BASE_URL="https://github.com/yusing/go-proxy/raw/${BRANCH}"
mkdir -p go-proxy
cd go-proxy
mkdir -p config
mkdir -p certs
[ -f compose.yml ] || wget -cO - ${BASE_URL}/compose.example.yml > compose.yml
[ -f config/config.yml ] || wget -cO - ${BASE_URL}/config.example.yml > config/config.yml
[ -f config/providers.yml ] || touch config/providers.yml

38
src/go-proxy/args.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"flag"
"github.com/sirupsen/logrus"
)
type Args struct {
Command string
}
const (
CommandStart = ""
CommandValidate = "validate"
CommandReload = "reload"
)
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
func getArgs() Args {
var args Args
flag.Parse()
args.Command = flag.Arg(0)
if err := validateArgs(args.Command, ValidCommands); err != nil {
logrus.Fatal(err)
}
return args
}
func validateArgs[T comparable](arg T, validArgs []T) error {
for _, v := range validArgs {
if arg == v {
return nil
}
}
return NewNestedError("invalid argument").Subjectf("%v", arg)
}

View File

@@ -21,9 +21,9 @@ import (
"github.com/go-acme/lego/v4/registration"
)
type ProviderOptions = map[string]string
type ProviderGenerator = func(ProviderOptions) (challenge.Provider, error)
type CertExpiries = map[string]time.Time
type ProviderOptions map[string]string
type ProviderGenerator func(ProviderOptions) (challenge.Provider, error)
type CertExpiries map[string]time.Time
type AutoCertConfig struct {
Email string `json:"email"`

View File

@@ -3,12 +3,14 @@ package main
import (
"os"
"sync"
"time"
"gopkg.in/yaml.v3"
)
// commented out if unused
type Config interface {
Value() configModel
// Load() error
MustLoad()
GetAutoCertProvider() (AutoCertProvider, error)
@@ -21,7 +23,9 @@ type Config interface {
}
func NewConfig(path string) Config {
cfg := &config{reader: &FileReader{Path: path}}
cfg := &config{
reader: &FileReader{Path: path},
}
cfg.watcher = NewFileWatcher(
path,
cfg.MustReload, // OnChange
@@ -35,6 +39,10 @@ func ValidateConfig(data []byte) error {
return cfg.Load()
}
func (cfg *config) Value() configModel {
return *cfg.m
}
func (cfg *config) Load(reader ...Reader) error {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
@@ -48,7 +56,7 @@ func (cfg *config) Load(reader ...Reader) error {
return NewNestedError("unable to read config file").With(err)
}
model := &configModel{}
model := defaultConfig()
if err := yaml.Unmarshal(data, model); err != nil {
return NewNestedError("unable to parse config file").With(err)
}
@@ -170,12 +178,21 @@ func (cfg *config) StopWatching() {
}
type configModel struct {
Providers map[string]*Provider `yaml:",flow" json:"providers"`
AutoCert AutoCertConfig `yaml:",flow" json:"autocert"`
Providers map[string]*Provider `yaml:",flow" json:"providers"`
AutoCert AutoCertConfig `yaml:",flow" json:"autocert"`
TimeoutShutdown time.Duration `yaml:"timeout_shutdown" json:"timeout_shutdown"`
RedirectToHTTPS bool `yaml:"redirect_to_https" json:"redirect_to_https"`
}
func defaultConfig() *configModel {
return &configModel{
TimeoutShutdown: 3 * time.Second,
RedirectToHTTPS: false,
}
}
type config struct {
m *configModel
m *configModel
reader Reader
watcher Watcher

View File

@@ -12,7 +12,7 @@ import (
)
var (
ImageNamePortMap = map[string]string{
ImageNamePortMapTCP = map[string]string{
"postgres": "5432",
"mysql": "3306",
"mariadb": "3306",
@@ -22,7 +22,7 @@ var (
"rabbitmq": "5672",
"mongo": "27017",
}
ExtraNamePortMap = map[string]string{
ExtraNamePortMapTCP = map[string]string{
"dns": "53",
"ssh": "22",
"ftp": "21",
@@ -30,18 +30,44 @@ var (
"pop3": "110",
"imap": "143",
}
NamePortMap = func() map[string]string {
NamePortMapTCP = func() map[string]string {
m := make(map[string]string)
for k, v := range ImageNamePortMap {
for k, v := range ImageNamePortMapTCP {
m[k] = v
}
for k, v := range ExtraNamePortMap {
for k, v := range ExtraNamePortMapTCP {
m[k] = v
}
return m
}()
)
var ImageNamePortMapHTTP = map[string]uint16{
"nginx": 80,
"httpd": 80,
"adguardhome": 3000,
"gogs": 3000,
"gitea": 3000,
"portainer": 9000,
"portainer-ce": 9000,
"home-assistant": 8123,
"homebridge": 8581,
"uptime-kuma": 3001,
"changedetection.io": 3000,
"prometheus": 9090,
"grafana": 3000,
"dockge": 5001,
"nginx-proxy-manager": 81,
}
var wellKnownHTTPPorts = map[uint16]bool{
80: true,
8000: true,
8008: true,
8080: true,
3000: true,
}
var (
StreamSchemes = []string{StreamType_TCP, StreamType_UDP} // TODO: support "tcp:udp", "udp:tcp"
HTTPSchemes = []string{"http", "https"}
@@ -92,6 +118,7 @@ var (
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
)
@@ -149,4 +176,7 @@ var logLevel = func() logrus.Level {
return logrus.GetLevel()
}()
var redirectToHTTPS = os.Getenv("GOPROXY_REDIRECT_HTTP") != "0" && os.Getenv("GOPROXY_REDIRECT_HTTP") != "false"
var isRunningAsService = func() bool {
v := os.Getenv("IS_SYSTEMD")
return v == "1"
}()

View File

@@ -23,7 +23,7 @@ func (p *Provider) setConfigField(c *ProxyConfig, label string, value string, pr
return nil
}
func (p *Provider) getContainerProxyConfigs(container types.Container, clientIP string) ProxyConfigSlice {
func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) ProxyConfigSlice {
var aliases []string
cfgs := make(ProxyConfigSlice, 0)
@@ -61,10 +61,10 @@ func (p *Provider) getContainerProxyConfigs(container types.Container, clientIP
}
}
if config.Port == "" {
config.Port = fmt.Sprintf("%d", selectPort(container))
config.Port = fmt.Sprintf("%d", selectPort(container, isRemote))
}
if config.Port == "0" {
l.Debugf("no ports exposed, ignored")
l.Infof("no ports exposed, ignored")
continue
}
if config.Scheme == "" {
@@ -74,10 +74,8 @@ func (p *Provider) getContainerProxyConfigs(container types.Container, clientIP
case strings.HasPrefix(container.Image, "sha256:"):
config.Scheme = "http"
default:
imageSplit := strings.Split(container.Image, "/")
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
imageName := imageSplit[0]
_, isKnownImage := ImageNamePortMap[imageName]
imageName := getImageName(container)
_, isKnownImage := ImageNamePortMapTCP[imageName]
if isKnownImage {
config.Scheme = "tcp"
} else {
@@ -182,25 +180,46 @@ func (p *Provider) getDockerProxyConfigs() (ProxyConfigSlice, error) {
cfgs := make(ProxyConfigSlice, 0)
for _, container := range containerSlice {
cfgs = append(cfgs, p.getContainerProxyConfigs(container, clientIP)...)
cfgs = append(cfgs, p.getContainerProxyConfigs(&container, clientIP)...)
}
return cfgs, nil
}
// var dockerUrlRegex = regexp.MustCompile(`^(?P<scheme>\w+)://(?P<host>[^:]+)(?P<port>:\d+)?(?P<path>/.*)?$`)
func getImageName(c *types.Container) string {
imageSplit := strings.Split(c.Image, "/")
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
return imageSplit[0]
}
func getPublicPort(p types.Port) uint16 { return p.PublicPort }
func getPrivatePort(p types.Port) uint16 { return p.PrivatePort }
func selectPort(c types.Container) uint16 {
if c.HostConfig.NetworkMode == "host" {
return selectPortInternal(c, getPrivatePort)
func selectPort(c *types.Container, isRemote bool) uint16 {
if isRemote || c.HostConfig.NetworkMode == "host" {
return selectPortInternal(c, getPublicPort)
}
return selectPortInternal(c, getPublicPort)
return selectPortInternal(c, getPrivatePort)
}
func selectPortInternal(c types.Container, getPort func(types.Port) uint16) uint16 {
func selectPortInternal(c *types.Container, getPort func(types.Port) uint16) uint16 {
imageName := getImageName(c)
// if is known image -> use known port
if port, isKnown := ImageNamePortMapHTTP[imageName]; isKnown {
for _, p := range c.Ports {
if p.PrivatePort == port {
return getPort(p)
}
}
}
// if it has known http port -> use it
for _, p := range c.Ports {
if isWellKnownHTTPPort(p.PrivatePort) {
return getPort(p)
}
}
// if it has any port -> use it
for _, p := range c.Ports {
if port := getPort(p); port != 0 {
return port
@@ -208,3 +227,8 @@ func selectPortInternal(c types.Container, getPort func(types.Port) uint16) uint
}
return 0
}
func isWellKnownHTTPPort(port uint16) bool {
_, ok := wellKnownHTTPPorts[port]
return ok
}

View File

@@ -1,23 +0,0 @@
package main
import "os"
type Reader interface {
Read() ([]byte, error)
}
type FileReader struct {
Path string
}
func (r *FileReader) Read() ([]byte, error) {
return os.ReadFile(r.Path)
}
type ByteReader struct {
Data []byte
}
func (r *ByteReader) Read() ([]byte, error) {
return r.Data, nil
}

View File

@@ -44,8 +44,8 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
PathMode: config.PathMode,
l: hrlog.WithFields(logrus.Fields{
"alias": config.Alias,
"path": config.Path,
"path_mode": config.PathMode,
// "path": config.Path,
// "path_mode": config.PathMode,
}),
}
@@ -157,6 +157,6 @@ func (config *ProxyConfig) pathSubModResp(r *http.Response) error {
}
// alias -> (path -> routes)
type HTTPRoutes = SafeMap[string, pathPoolMap]
type HTTPRoutes SafeMap[string, pathPoolMap]
var httpRoutes HTTPRoutes = NewSafeMapOf[HTTPRoutes](newPathPoolMap)

View File

@@ -2,13 +2,37 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"sync"
"os"
"sync/atomic"
)
type Reader interface {
Read() ([]byte, error)
}
type FileReader struct {
Path string
}
func (r *FileReader) Read() ([]byte, error) {
return os.ReadFile(r.Path)
}
type ByteReader struct {
Data []byte
}
func (r *ByteReader) Read() ([]byte, error) {
return r.Data, nil
}
type ReadCloser struct {
ctx context.Context
r io.ReadCloser
ctx context.Context
r io.ReadCloser
closed atomic.Bool
}
func (r *ReadCloser) Read(p []byte) (int, error) {
@@ -21,13 +45,16 @@ func (r *ReadCloser) Read(p []byte) (int, error) {
}
func (r *ReadCloser) Close() error {
if r.closed.Load() {
return nil
}
r.closed.Store(true)
return r.r.Close()
}
type Pipe struct {
r ReadCloser
w io.WriteCloser
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
@@ -35,32 +62,24 @@ type Pipe struct {
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
ctx, cancel := context.WithCancel(ctx)
return &Pipe{
r: ReadCloser{ctx, r},
r: ReadCloser{ctx: ctx, r: r},
w: w,
ctx: ctx,
cancel: cancel,
}
}
func (p *Pipe) Start() {
p.wg.Add(1)
go func() {
Copy(p.ctx, p.w, &p.r)
p.wg.Done()
}()
func (p *Pipe) Start() error {
return Copy(p.ctx, p.w, &p.r)
}
func (p *Pipe) Stop() {
func (p *Pipe) Stop() error {
p.cancel()
p.wg.Wait()
return errors.Join(fmt.Errorf("read: %w", p.r.Close()), fmt.Errorf("write: %w", p.w.Close()))
}
func (p *Pipe) Close() (error, error) {
return p.r.Close(), p.w.Close()
}
func (p *Pipe) Wait() {
p.wg.Wait()
func (p *Pipe) Write(b []byte) (int, error) {
return p.w.Write(b)
}
type BidirectionalPipe struct {
@@ -75,26 +94,34 @@ func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.Re
}
}
func (p *BidirectionalPipe) Start() {
p.pSrcDst.Start()
p.pDstSrc.Start()
func NewBidirectionalPipeIntermediate(ctx context.Context, listener io.ReadCloser, client io.ReadWriteCloser, target io.ReadWriteCloser) *BidirectionalPipe {
return &BidirectionalPipe{
pSrcDst: *NewPipe(ctx, listener, client),
pDstSrc: *NewPipe(ctx, client, target),
}
}
func (p *BidirectionalPipe) Stop() {
p.pSrcDst.Stop()
p.pDstSrc.Stop()
func (p *BidirectionalPipe) Start() error {
errCh := make(chan error, 2)
go func() {
errCh <- p.pSrcDst.Start()
}()
go func() {
errCh <- p.pDstSrc.Start()
}()
for err := range errCh {
if err != nil {
return err
}
}
return nil
}
func (p *BidirectionalPipe) Close() (error, error) {
return p.pSrcDst.Close()
}
func (p *BidirectionalPipe) Wait() {
p.pSrcDst.Wait()
p.pDstSrc.Wait()
func (p *BidirectionalPipe) Stop() error {
return errors.Join(p.pSrcDst.Stop(), p.pDstSrc.Stop())
}
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) error {
_, err := io.Copy(dst, &ReadCloser{ctx, src})
_, err := io.Copy(dst, &ReadCloser{ctx: ctx, r: src})
return err
}

View File

@@ -1,12 +1,13 @@
package main
import (
"flag"
"net/http"
"os"
"os/signal"
"runtime"
"sync"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
@@ -16,21 +17,36 @@ var cfg Config
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var verifyOnly bool
flag.BoolVar(&verifyOnly, "verify", false, "verify config without starting server")
flag.Parse()
args := getArgs()
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableColors: false,
FullTimestamp: true,
TimestampFormat: "01-02 15:04:05",
})
if isRunningAsService {
logrus.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
DisableTimestamp: true,
DisableSorting: true,
})
} else {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableColors: false,
DisableSorting: true,
FullTimestamp: true,
TimestampFormat: "01-02 15:04:05",
})
}
if args.Command == CommandReload {
err := utils.reloadServer()
if err != nil {
logrus.Fatal(err)
}
return
}
cfg = NewConfig(configPath)
cfg.MustLoad()
if verifyOnly {
if args.Command == CommandValidate {
logrus.Printf("config OK")
return
}
@@ -63,7 +79,7 @@ func main() {
HTTPAddr: ":80",
HTTPSAddr: ":443",
Handler: http.HandlerFunc(proxyHandler),
RedirectToHTTPS: redirectToHTTPS,
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
})
panelServer = NewServer(ServerOptions{
Name: "panel",
@@ -71,7 +87,7 @@ func main() {
HTTPAddr: ":8080",
HTTPSAddr: ":8443",
Handler: panelHandler,
RedirectToHTTPS: redirectToHTTPS,
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
})
proxyServer.Start()
@@ -88,10 +104,32 @@ func main() {
signal.Notify(sig, syscall.SIGHUP)
<-sig
// cfg.StopWatching()
StopFSWatcher()
StopDockerWatcher()
cfg.StopProviders()
panelServer.Stop()
proxyServer.Stop()
logrus.Info("shutting down")
done := make(chan struct{}, 1)
var wg sync.WaitGroup
wg.Add(3)
go func() {
StopFSWatcher()
StopDockerWatcher()
cfg.StopProviders()
wg.Done()
}()
go func() {
panelServer.Stop()
proxyServer.Stop()
wg.Done()
}()
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
logrus.Info("shutdown complete")
case <-time.After(cfg.Value().TimeoutShutdown * time.Second):
logrus.Info("timeout waiting for shutdown")
}
}

View File

@@ -2,6 +2,7 @@ package main
import (
"errors"
"fmt"
"html/template"
"net/http"
"net/url"
@@ -68,7 +69,7 @@ func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) {
func panelConfigEditor(w http.ResponseWriter, r *http.Request) {
cfgFiles := make([]string, 0)
cfgFiles = append(cfgFiles, path.Base(configPath))
for _, p := range cfg.(*config).m.Providers {
for _, p := range cfg.Value().Providers {
if p.Kind != ProviderKind_File {
continue
}
@@ -99,12 +100,20 @@ func panelConfigUpdate(w http.ResponseWriter, r *http.Request) {
panelHandleErr(w, r, err)
return
}
err = os.WriteFile(path.Join(configBasePath, p), content, 0644)
p = path.Join(configBasePath, p)
_, err = os.Stat(p)
exists := !errors.Is(err, os.ErrNotExist)
err = os.WriteFile(p, content, 0644)
if err != nil {
panelHandleErr(w, r, NewNestedError("unable to write config file").With(err))
return
}
w.WriteHeader(http.StatusOK)
if !exists {
w.Write([]byte(fmt.Sprintf("Config file %s created, remember to add it to config.yml!", p)))
return
}
w.Write([]byte(fmt.Sprintf("Config file %s updated", p)))
}
func panelServeFile(w http.ResponseWriter, r *http.Request) {
@@ -141,4 +150,4 @@ func panelHandleErr(w http.ResponseWriter, r *http.Request, err error, code ...i
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -15,8 +15,8 @@ type ProxyConfig struct {
provider *Provider
}
type ProxyConfigMap = map[string]ProxyConfig
type ProxyConfigSlice = []ProxyConfig
type ProxyConfigMap map[string]ProxyConfig
type ProxyConfigSlice []ProxyConfig
func NewProxyConfig(provider *Provider) ProxyConfig {
return ProxyConfig{

View File

@@ -15,7 +15,6 @@ func NewRoute(cfg *ProxyConfig) (Route, error) {
if err != nil {
return nil, NewNestedErrorFrom(err).Subject(cfg.Alias)
}
streamRoutes.Set(id, route)
return route, nil
} else {
httpRoutes.Ensure(cfg.Alias)
@@ -47,6 +46,6 @@ func isStreamScheme(s string) bool {
}
// id -> target
type StreamRoutes = SafeMap[string, StreamRoute]
type StreamRoutes SafeMap[string, StreamRoute]
var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]()

View File

@@ -31,11 +31,11 @@ type ServerOptions struct {
}
type LogrusWrapper struct {
l *logrus.Entry
*logrus.Entry
}
func (l LogrusWrapper) Write(b []byte) (int, error) {
return l.l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
}
func NewServer(opt ServerOptions) *Server {

View File

@@ -45,10 +45,8 @@ type StreamRouteBase struct {
func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
var streamType string = StreamType_TCP
var srcPort string
var dstPort string
var srcScheme string
var dstScheme string
var srcPort, dstPort string
var srcScheme, dstScheme string
portSplit := strings.Split(config.Port, ":")
if len(portSplit) != 2 {
@@ -60,7 +58,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
dstPort = portSplit[1]
}
if port, hasName := NamePortMap[dstPort]; hasName {
if port, hasName := NamePortMapTCP[dstPort]; hasName {
dstPort = port
}
@@ -85,6 +83,10 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
dstScheme = config.Scheme
}
if srcScheme != dstScheme {
return nil, NewNestedError("unsupported").Subjectf("%v -> %v", srcScheme, dstScheme)
}
return &StreamRouteBase{
Alias: config.Alias,
Type: streamType,
@@ -101,21 +103,26 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
started: false,
l: srlog.WithFields(logrus.Fields{
"alias": config.Alias,
"src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt),
"dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt),
// "src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt),
// "dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt),
}),
}, nil
}
func NewStreamRoute(config *ProxyConfig) (StreamRoute, error) {
base, err := newStreamRouteBase(config)
if err != nil {
return nil, err
}
switch config.Scheme {
case StreamType_TCP:
return NewTCPRoute(config)
base.StreamImpl = NewTCPRoute(base)
case StreamType_UDP:
return NewUDPRoute(config)
base.StreamImpl = NewUDPRoute(base)
default:
return nil, NewNestedError("invalid stream type").Subject(config.Scheme)
}
return base, nil
}
func (route *StreamRouteBase) ListeningUrl() string {
@@ -136,6 +143,7 @@ func (route *StreamRouteBase) Start() {
route.l.Errorf("failed to setup: %v", err)
return
}
streamRoutes.Set(route.id, route)
route.started = true
route.wg.Add(2)
go route.grAcceptConnections()

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"sync"
"time"
)
@@ -14,21 +15,16 @@ type Pipes []*BidirectionalPipe
type TCPRoute struct {
*StreamRouteBase
listener net.Listener
pipe Pipes
mu sync.Mutex
}
func NewTCPRoute(config *ProxyConfig) (StreamRoute, error) {
base, err := newStreamRouteBase(config)
if err != nil {
return nil, NewNestedErrorFrom(err).Subject(config.Alias)
}
if base.TargetScheme != StreamType_TCP {
return nil, NewNestedError("unsupported").Subjectf("tcp -> %s", base.TargetScheme)
}
base.StreamImpl = &TCPRoute{
func NewTCPRoute(base *StreamRouteBase) StreamImpl {
return &TCPRoute{
StreamRouteBase: base,
listener: nil,
pipe: make(Pipes, 0),
}
return base, nil
}
func (route *TCPRoute) Setup() error {
@@ -44,11 +40,10 @@ func (route *TCPRoute) Accept() (interface{}, error) {
return route.listener.Accept()
}
func (route *TCPRoute) HandleConnection(c interface{}) error {
func (route *TCPRoute) Handle(c interface{}) error {
clientConn := c.(net.Conn)
defer clientConn.Close()
defer route.wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), tcpDialTimeout)
defer cancel()
@@ -66,11 +61,12 @@ func (route *TCPRoute) HandleConnection(c interface{}) error {
<-route.stopCh
pipeCancel()
}()
route.mu.Lock()
pipe := NewBidirectionalPipe(pipeCtx, clientConn, serverConn)
pipe.Start()
pipe.Wait()
pipe.Close()
return nil
route.pipe = append(route.pipe, pipe)
route.mu.Unlock()
return pipe.Start()
}
func (route *TCPRoute) CloseListeners() {
@@ -79,4 +75,9 @@ func (route *TCPRoute) CloseListeners() {
}
route.listener.Close()
route.listener = nil
for _, pipe := range route.pipe {
if err := pipe.Stop(); err != nil {
route.l.Error(err)
}
}
}

View File

@@ -1,62 +1,55 @@
package main
import (
"context"
"fmt"
"io"
"net"
"sync"
"github.com/sirupsen/logrus"
)
type UDPRoute struct {
*StreamRouteBase
connMap map[net.Addr]net.Conn
connMap UDPConnMap
connMapMutex sync.Mutex
listeningConn *net.UDPConn
targetConn *net.UDPConn
targetAddr *net.UDPAddr
}
type UDPConn struct {
remoteAddr net.Addr
buffer []byte
bytesReceived []byte
nReceived int
src *net.UDPConn
dst *net.UDPConn
*BidirectionalPipe
}
func NewUDPRoute(config *ProxyConfig) (StreamRoute, error) {
base, err := newStreamRouteBase(config)
if err != nil {
return nil, err
}
type UDPConnMap map[net.Addr]*UDPConn
if base.TargetScheme != StreamType_UDP {
return nil, NewNestedError("unsupported").Subjectf("udp->%s", base.TargetScheme)
}
base.StreamImpl = &UDPRoute{
func NewUDPRoute(base *StreamRouteBase) StreamImpl {
return &UDPRoute{
StreamRouteBase: base,
connMap: make(map[net.Addr]net.Conn),
connMap: make(UDPConnMap),
}
return base, nil
}
func (route *UDPRoute) Setup() error {
source, err := net.ListenPacket(route.ListeningScheme, fmt.Sprintf(":%v", route.ListeningPort))
laddr, err := net.ResolveUDPAddr(route.ListeningScheme, fmt.Sprintf(":%v", route.ListeningPort))
if err != nil {
return err
}
target, err := net.Dial(route.TargetScheme, fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort))
source, err := net.ListenUDP(route.ListeningScheme, laddr)
if err != nil {
return err
}
raddr, err := net.ResolveUDPAddr(route.TargetScheme, fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort))
if err != nil {
source.Close()
return err
}
route.listeningConn = source.(*net.UDPConn)
route.targetConn = target.(*net.UDPConn)
route.listeningConn = source
route.targetAddr = raddr
return nil
}
@@ -74,71 +67,39 @@ func (route *UDPRoute) Accept() (interface{}, error) {
return nil, io.ErrShortBuffer
}
conn := &UDPConn{
remoteAddr: srcAddr,
buffer: buffer,
bytesReceived: buffer[:nRead],
nReceived: nRead,
}
return conn, nil
}
conn, ok := route.connMap[srcAddr]
func (route *UDPRoute) HandleConnection(c interface{}) error {
var err error
conn := c.(*UDPConn)
srcConn, ok := route.connMap[conn.remoteAddr]
if !ok {
route.connMapMutex.Lock()
srcConn, err = net.DialUDP("udp", nil, conn.remoteAddr.(*net.UDPAddr))
srcConn, err := net.DialUDP("udp", nil, srcAddr)
if err != nil {
return err
return nil, err
}
route.connMap[conn.remoteAddr] = srcConn
dstConn, err := net.DialUDP("udp", nil, route.targetAddr)
if err != nil {
srcConn.Close()
return nil, err
}
pipeCtx, pipeCancel := context.WithCancel(context.Background())
go func() {
<-route.stopCh
pipeCancel()
}()
conn = &UDPConn{
srcConn,
dstConn,
NewBidirectionalPipe(pipeCtx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
}
route.connMap[srcAddr] = conn
route.connMapMutex.Unlock()
}
var forwarder func(*UDPConn, net.Conn) error
_, err = conn.dst.Write(buffer[:nRead])
return conn, err
}
if logLevel == logrus.DebugLevel {
forwarder = route.forwardReceivedDebug
} else {
forwarder = route.forwardReceivedReal
}
// initiate connection to target
err = forwarder(conn, route.targetConn)
if err != nil {
return err
}
for {
select {
case <-route.stopCh:
return nil
default:
// receive from target
conn, err = route.readFrom(route.targetConn, conn.buffer)
if err != nil {
return err
}
// forward to source
err = forwarder(conn, srcConn)
if err != nil {
return err
}
// read from source
conn, err = route.readFrom(srcConn, conn.buffer)
if err != nil {
continue
}
// forward to target
err = forwarder(conn, route.targetConn)
if err != nil {
return err
}
}
}
func (route *UDPRoute) Handle(c interface{}) error {
return c.(*UDPConn).Start()
}
func (route *UDPRoute) CloseListeners() {
@@ -146,50 +107,28 @@ func (route *UDPRoute) CloseListeners() {
route.listeningConn.Close()
route.listeningConn = nil
}
if route.targetConn != nil {
route.targetConn.Close()
route.targetConn = nil
}
for _, conn := range route.connMap {
conn.(*net.UDPConn).Close() // TODO: change on non udp target
if err := conn.dst.Close(); err != nil {
route.l.Error(err)
}
}
route.connMap = make(map[net.Addr]net.Conn)
route.connMap = make(UDPConnMap)
}
func (route *UDPRoute) readFrom(src net.Conn, buffer []byte) (*UDPConn, error) {
nRead, err := src.Read(buffer)
if err != nil {
return nil, err
}
if nRead == 0 {
return nil, io.ErrShortBuffer
}
return &UDPConn{
remoteAddr: src.RemoteAddr(),
buffer: buffer,
bytesReceived: buffer[:nRead],
nReceived: nRead,
}, nil
type sourceRWCloser struct {
server *net.UDPConn
target *net.UDPConn
}
func (route *UDPRoute) forwardReceivedReal(receivedConn *UDPConn, dest net.Conn) error {
nWritten, err := dest.Write(receivedConn.bytesReceived)
if nWritten != receivedConn.nReceived {
err = io.ErrShortWrite
}
return err
func (w sourceRWCloser) Read(p []byte) (int, error) {
n, _, err := w.target.ReadFrom(p)
return n, err
}
func (route *UDPRoute) forwardReceivedDebug(receivedConn *UDPConn, dest net.Conn) error {
route.l.WithField("size", receivedConn.nReceived).Debugf(
"forwarding from %s to %s",
receivedConn.remoteAddr.String(),
dest.RemoteAddr().String(),
)
return route.forwardReceivedReal(receivedConn, dest)
func (w sourceRWCloser) Write(p []byte) (int, error) {
return w.server.WriteToUDP(p, w.target.RemoteAddr().(*net.UDPAddr)) // TODO: support non udp
}
func (w sourceRWCloser) Close() error {
return w.target.Close()
}

View File

@@ -94,6 +94,18 @@ func (*Utils) healthCheckStream(scheme, host string) error {
return nil
}
func (*Utils) reloadServer() error {
resp, err := healthCheckHttpClient.Post("http://localhost:8080/reload", "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return NewNestedError("server reload failed").Subjectf("%d", resp.StatusCode)
}
return nil
}
func (*Utils) snakeToPascal(s string) string {
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
return strings.ReplaceAll(toHyphenCamel, "-", "")

View File

@@ -89,7 +89,7 @@ func (w *fileWatcher) Stop() {
fileWatchMap.Delete(w.path)
err := fsWatcher.Remove(w.path)
if err != nil {
w.l.WithField("action", "stop").Error(err)
w.l.Error(err)
}
}

View File

@@ -18,6 +18,9 @@
<a class="unselectable">{{$cfgFile}}</a>
</li>
{{- end}}
<li id="new-file">
<a class="unselectable">+</a>
</li>
</ul>
</div>
<div id="config-editor"></div>

View File

@@ -11,25 +11,43 @@ let editor = CodeMirror(editorElement, {
tabSize: 2
});
function loadFile(fileName) {
if (fileName === undefined) {
function setCurrentFile(filename) {
let old_nav_item = document.getElementById(`file-${currentFile}`);
if (old_nav_item !== null) {
old_nav_item.classList.remove("active");
}
currentFile = filename;
document.title = `${currentFile} - Config Editor`;
let new_nav_item = document.getElementById(`file-${currentFile}`);
if (new_nav_item === null) {
new_file_btn = document.getElementById("new-file");
file_list = document.getElementById("file-list");
new_nav_item = document.createElement("li");
new_nav_item.id = `file-${currentFile}`;
new_nav_item.innerHTML = `<a class="unselectable">${currentFile}</a>`;
file_list.insertBefore(new_nav_item, new_file_btn);
}
new_nav_item.classList.add("active");
}
function loadFile(filename) {
if (filename === undefined) {
return;
}
if (filename === '+') {
newFile();
return;
}
let req = new XMLHttpRequest();
req.open("GET", `/config/${fileName}`, true);
req.open("GET", `/config/${filename}`, true);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
let old_nav_item = document.getElementById(`file-${currentFile}`);
old_nav_item.classList.remove("active");
editor.setValue(req.responseText);
currentFile = fileName;
let new_nav_item = document.getElementById(`file-${currentFile}`);
new_nav_item.classList.add("active");
document.title = `${currentFile} - Config Editor`;
setCurrentFile(filename);
console.log(`loaded ${currentFile}`);
} else {
let msg = `Failed to load ${fileName}: ` + req.responseText;
let msg = `Failed to load ${filename}: ` + req.responseText;
alert(msg);
console.log(msg);
}
@@ -46,14 +64,35 @@ function saveFile(filename, content) {
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
alert("Saved " + filename);
alert(req.responseText);
} else {
alert("Error: " + req.responseText);
alert("Error:\n" + req.responseText);
}
}
};
}
function newFile() {
let filename = prompt("Enter filename:");
if (filename === undefined || filename === "") {
alert("File name cannot be empty");
return;
}
if (!filename.endsWith(".yml") && !filename.endsWith(".yaml")) {
alert("File name must end with .yml or .yaml");
return;
}
let files = document.getElementById("file-list").children;
for (let i = 0; i < files.length; i++) {
if (files[i].id === `file-${filename}`) {
alert("File already exists");
return;
}
}
editor.setValue("");
setCurrentFile(filename);
}
editor.setSize("100wh", "100vh");
editor.setOption("extraKeys", {
Tab: function (cm) {

View File

@@ -36,6 +36,10 @@ body {
padding-right: 4em;
display: block;
}
#new-file {
color: #f8f8f2 !important;
font-weight: bold;
}
.active {
font-weight: bold;
background: rgba(255, 255, 255, 0.1);

1
version.txt Normal file
View File

@@ -0,0 +1 @@
0.4.5