mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-19 08:57:42 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e5a8d0985 | ||
|
|
16b507bc7c | ||
|
|
1120991019 | ||
|
|
c0ebd9f8c0 |
7
Makefile
7
Makefile
@@ -36,4 +36,9 @@ repush:
|
||||
git reset --soft HEAD^
|
||||
git add -A
|
||||
git commit -m "repush"
|
||||
git push gitlab dev --force
|
||||
git push gitlab dev --force
|
||||
|
||||
rapid-crash:
|
||||
sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\
|
||||
sleep 3 &&\
|
||||
sudo docker rm -f test_crash
|
||||
|
||||
15
README.md
15
README.md
@@ -24,8 +24,7 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
- Auto configuration for docker contaienrs
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Support HTTP(s), TCP and UDP
|
||||
- Support HTTP(s) round robin load balancing
|
||||
- Web UI for configuration and monitoring (See [screenshots](screeenshots))
|
||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -110,14 +109,16 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
## Build it yourself
|
||||
|
||||
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
|
||||
2. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
|
||||
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
|
||||
3. get dependencies with `make get`
|
||||
3. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
|
||||
|
||||
4. build binary with `make build`
|
||||
4. get dependencies with `make get`
|
||||
|
||||
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
5. build binary with `make build`
|
||||
|
||||
6. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -5,7 +5,8 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8888
|
||||
depends_on:
|
||||
- app
|
||||
app:
|
||||
|
||||
@@ -85,24 +85,26 @@
|
||||
|
||||
### Syntax
|
||||
|
||||
| Label | Description |
|
||||
| ----------------------- | -------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias |
|
||||
| `proxy.*.<field>` | set field for all aliases |
|
||||
| Label | Description | Default |
|
||||
| ----------------------- | -------------------------------------------------------- | ---------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | N/A |
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: `container_name`</li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### Key-value mapping example
|
||||
|
||||
@@ -132,6 +134,8 @@ service_a:
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### List example
|
||||
|
||||
Docker Compose
|
||||
@@ -166,6 +170,23 @@ service_a:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Container not showing up in proxies list
|
||||
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, i.e.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
@@ -237,6 +258,8 @@ services:
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
@@ -251,7 +274,8 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8888
|
||||
depends_on:
|
||||
- go-proxy
|
||||
```
|
||||
|
||||
2
frontend
2
frontend
Submodule frontend updated: 8cdf9eaa10...d0e59630d6
@@ -25,7 +25,7 @@ func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{All: true}))
|
||||
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{}))
|
||||
if err.IsNotNil() {
|
||||
return nil, E.Failure("list containers").With(err)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ func Failure(what string) NestedError {
|
||||
return errorf("%s %w", what, ErrFailure)
|
||||
}
|
||||
|
||||
func FailureWhy(what string, why string) NestedError {
|
||||
return errorf("%s %w because %s", what, ErrFailure, why)
|
||||
}
|
||||
|
||||
func Invalid(subject, what any) NestedError {
|
||||
return errorf("%w %v - %v", ErrInvalid, subject, what)
|
||||
}
|
||||
|
||||
@@ -39,10 +39,12 @@ func (e *ProxyEntry) SetDefaults() {
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
if e.Port == "" {
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func NewPort(v string) (Port, E.NestedError) {
|
||||
return NewPortInt(p)
|
||||
}
|
||||
|
||||
func NewPortInt(v int) (Port, E.NestedError) {
|
||||
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
pp := Port(v)
|
||||
if err := pp.boundCheck(); err.IsNotNil() {
|
||||
return ErrPort, err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
@@ -39,10 +40,10 @@ func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost)
|
||||
if err.IsNotNil() {
|
||||
return entries, E.From(err)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost)
|
||||
errors := E.NewBuilder("errors when parse docker labels")
|
||||
|
||||
for _, container := range info.Containers {
|
||||
en, err := p.getEntriesFromLabels(&container, info.Host)
|
||||
@@ -93,9 +94,9 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
// find first port, return if no port exposed
|
||||
defaultPort := findFirstPort(container)
|
||||
if defaultPort == PT.NoPort {
|
||||
return entries, E.Nil()
|
||||
defaultPort, err := findFirstPort(container)
|
||||
if err.IsNotNil() {
|
||||
logrus.Debug(mainAlias, " ", err.Error())
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
@@ -103,7 +104,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
entries.Set(string(a), &M.ProxyEntry{
|
||||
Alias: string(a),
|
||||
Host: clientHost,
|
||||
Port: fmt.Sprint(defaultPort),
|
||||
Port: defaultPort,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -136,15 +137,23 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
}
|
||||
}
|
||||
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
if e.Port == "" {
|
||||
entries.UnsafeDelete(a)
|
||||
}
|
||||
})
|
||||
|
||||
return entries, errors.Build()
|
||||
}
|
||||
|
||||
func findFirstPort(c *types.Container) (pp PT.Port) {
|
||||
func findFirstPort(c *types.Container) (string, E.NestedError) {
|
||||
if len(c.Ports) == 0 {
|
||||
return "", E.FailureWhy("findFirstPort", "no port exposed")
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
if p.PublicPort != 0 || c.HostConfig.NetworkMode == "host" {
|
||||
pp, _ = PT.NewPortInt(int(p.PublicPort))
|
||||
return
|
||||
if p.PublicPort != 0 {
|
||||
return fmt.Sprint(p.PublicPort), E.Nil()
|
||||
}
|
||||
}
|
||||
return PT.NoPort
|
||||
return "", E.Failure("findFirstPort")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
@@ -30,6 +31,8 @@ type Provider struct {
|
||||
watcherCancel context.CancelFunc
|
||||
|
||||
l *logrus.Entry
|
||||
|
||||
cooldownCh chan struct{}
|
||||
}
|
||||
|
||||
type ProviderType string
|
||||
@@ -45,9 +48,10 @@ func newProvider(name string, t ProviderType) *Provider {
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
reloadReqCh: make(chan struct{}, 1),
|
||||
cooldownCh: make(chan struct{}, 1),
|
||||
}
|
||||
p.l = logrus.WithField("provider", p)
|
||||
|
||||
go p.processReloadRequests()
|
||||
return p
|
||||
}
|
||||
func NewFileProvider(filename string) *Provider {
|
||||
@@ -100,7 +104,8 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||
nStarted++
|
||||
}
|
||||
})
|
||||
p.l.Infof("%d routes started, %d failed", nStarted, nFailed)
|
||||
|
||||
p.l.Debugf("%d routes started, %d failed", nStarted, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
@@ -120,16 +125,17 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
nStopped++
|
||||
}
|
||||
})
|
||||
p.l.Infof("%d routes stopped, %d failed", nStopped, nFailed)
|
||||
p.l.Debugf("%d routes stopped, %d failed", nStopped, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
func (p *Provider) ReloadRoutes() {
|
||||
defer p.l.Info("routes reloaded")
|
||||
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
select {
|
||||
case p.reloadReqCh <- struct{}{}:
|
||||
// Successfully sent reload request
|
||||
default:
|
||||
// Reload request already in progress, ignore this request
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||
@@ -142,15 +148,14 @@ func (p *Provider) watchEvents() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.reloadReqCh: // block until last reload is done
|
||||
p.ReloadRoutes()
|
||||
continue // ignore events once after reload
|
||||
case <-p.watcherCtx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
l.Info(event)
|
||||
p.reloadReqCh <- struct{}{}
|
||||
p.ReloadRoutes()
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
return
|
||||
@@ -163,6 +168,30 @@ func (p *Provider) watchEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) processReloadRequests() {
|
||||
for range p.reloadReqCh {
|
||||
// prevent busy loop caused by a container
|
||||
// repeating crashing and restarting
|
||||
select {
|
||||
case p.cooldownCh <- struct{}{}:
|
||||
p.l.Info("Starting to reload routes")
|
||||
nRoutes := p.routes.Size()
|
||||
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
|
||||
p.l.Infof("Routes reloaded (%d -> %d)", nRoutes, p.routes.Size())
|
||||
|
||||
go func() {
|
||||
time.Sleep(reloadCooldown)
|
||||
<-p.cooldownCh
|
||||
}()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadRoutes() E.NestedError {
|
||||
entries, err := p.GetProxyEntries()
|
||||
|
||||
@@ -183,3 +212,5 @@ func (p *Provider) loadRoutes() E.NestedError {
|
||||
})
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
const reloadCooldown = 50 * time.Millisecond
|
||||
|
||||
@@ -19,10 +19,9 @@ import (
|
||||
|
||||
type (
|
||||
HTTPRoute struct {
|
||||
Alias PT.Alias `json:"alias"`
|
||||
|
||||
TargetURL URL
|
||||
PathPatterns PT.PathPatterns
|
||||
Alias PT.Alias `json:"alias"`
|
||||
TargetURL *URL `json:"target_url"`
|
||||
PathPatterns PT.PathPatterns `json:"path_patterns"`
|
||||
|
||||
mux *http.ServeMux
|
||||
handler *P.ReverseProxy
|
||||
@@ -53,7 +52,7 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||
if !ok {
|
||||
r = &HTTPRoute{
|
||||
Alias: entry.Alias,
|
||||
TargetURL: URL(*entry.URL),
|
||||
TargetURL: (*URL)(entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
handler: rp,
|
||||
}
|
||||
|
||||
@@ -135,6 +135,10 @@ func (m *Map[KT, VT]) Delete(key KT) {
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) UnsafeDelete(key KT) {
|
||||
delete(m.m, key)
|
||||
}
|
||||
|
||||
// MergeWith merges the contents of another Map[KT, VT]
|
||||
// into the current Map[KT, VT] and
|
||||
// returns a map that were duplicated.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
@@ -22,4 +23,4 @@ func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nested
|
||||
return fwHelper.Add(ctx, f)
|
||||
}
|
||||
|
||||
var fwHelper = newFileWatcherHelper()
|
||||
var fwHelper = newFileWatcherHelper(common.ConfigBasePath)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
@@ -26,14 +25,12 @@ type fileWatcherStream struct {
|
||||
errCh chan E.NestedError
|
||||
}
|
||||
|
||||
func newFileWatcherHelper() *fileWatcherHelper {
|
||||
func newFileWatcherHelper(dirPath string) *fileWatcherHelper {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
// watch config path for all changes
|
||||
err = w.Add(common.ConfigBasePath)
|
||||
if err != nil {
|
||||
if err = w.Add(dirPath); err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
helper := &fileWatcherHelper{
|
||||
@@ -60,26 +57,24 @@ func (h *fileWatcherHelper) Add(ctx context.Context, w *fileWatcher) (<-chan Eve
|
||||
errCh: make(chan E.NestedError),
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
h.Remove(w)
|
||||
return
|
||||
case <-s.stopped:
|
||||
return
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.stopped <- struct{}{}
|
||||
case <-s.stopped:
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
close(s.eventCh)
|
||||
close(s.errCh)
|
||||
delete(h.m, w.filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
h.m[w.filename] = s
|
||||
return s.eventCh, s.errCh
|
||||
}
|
||||
|
||||
func (h *fileWatcherHelper) Remove(w *fileWatcher) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.m[w.filename].stopped <- struct{}{}
|
||||
delete(h.m, w.filename)
|
||||
}
|
||||
|
||||
func (h *fileWatcherHelper) start() {
|
||||
defer h.wg.Done()
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.5.0-rc1
|
||||
0.5.0-rc3
|
||||
Reference in New Issue
Block a user