diff --git a/README.md b/README.md index 0349502e..f0ec0045 100755 --- a/README.md +++ b/README.md @@ -24,7 +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 -- 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) diff --git a/compose.example.yml b/compose.example.yml index 15752050..89508acd 100755 --- a/compose.example.yml +++ b/compose.example.yml @@ -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: diff --git a/docs/docker.md b/docs/docker.md index bfea1367..7ed2244c 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -85,24 +85,26 @@ ### Syntax -| Label | Description | -| ----------------------- | -------------------------------------------------------- | -| `proxy.aliases` | comma separated aliases for subdomain and label matching | -| `proxy..` | set field for specific alias | -| `proxy.*.` | set field for all aliases | +| Label | Description | Default | +| ----------------------- | -------------------------------------------------------- | ---------------- | +| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | +| `proxy..` | set field for specific alias | N/A | +| `proxy.*.` | set field for all aliases | N/A | ### Fields -| Field | Description | Default | Allowed Values / Syntax | -| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `scheme` | proxy protocol |
  • `http` for numeric port
  • `tcp` for `x:y` port
| `http`, `https`, `tcp`, `udp` | -| `host` | proxy host |
  • Docker: `container_name`
  • File: `localhost`
| 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`
  • x: port for `go-proxy` to listen on
  • y: port or [_service name_](../src/common/constants.go#L55) of target container
| -| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean | -| `path_patterns` | proxy path patterns **(http/s only)**
only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[1](#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[2](#key-value-mapping-example) of header-value pairs | -| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[1](#list-example) of headers | +| Field | Description | Default | Allowed Values / Syntax | +| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scheme` | proxy protocol |
  • `http` for numeric port
  • `tcp` for `x:y` port
| `http`, `https`, `tcp`, `udp` | +| `host` | proxy host |
  • Docker: docker client IP / hostname
  • File: `localhost`
| 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`
  • x: port for `go-proxy` to listen on
  • y: port or [_service name_](../src/common/constants.go#L55) of target container
| +| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean | +| `path_patterns` | proxy path patterns **(http/s only)**
only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[1](#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[2](#key-value-mapping-example) of header-value pairs | +| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[1](#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..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 ``` diff --git a/frontend b/frontend index 8cdf9eaa..d0e59630 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 8cdf9eaa10728ca808e479ae0144b5f7a62d9a88 +Subproject commit d0e59630d6e0beb1c22d2f242f556464a5056c1f diff --git a/src/docker/client_info.go b/src/docker/client_info.go index b12ad7ee..c6de93eb 100644 --- a/src/docker/client_info.go +++ b/src/docker/client_info.go @@ -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) } diff --git a/src/error/errors.go b/src/error/errors.go index 6000c703..98213b98 100644 --- a/src/error/errors.go +++ b/src/error/errors.go @@ -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) } diff --git a/src/proxy/fields/port.go b/src/proxy/fields/port.go index 2c9532f6..5da17889 100644 --- a/src/proxy/fields/port.go +++ b/src/proxy/fields/port.go @@ -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 diff --git a/src/proxy/provider/docker_provider.go b/src/proxy/provider/docker_provider.go index 6557985c..a68e6277 100755 --- a/src/proxy/provider/docker_provider.go +++ b/src/proxy/provider/docker_provider.go @@ -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") } diff --git a/src/proxy/provider/provider.go b/src/proxy/provider/provider.go index 97a89eaf..a9ad8f39 100644 --- a/src/proxy/provider/provider.go +++ b/src/proxy/provider/provider.go @@ -175,12 +175,13 @@ func (p *Provider) processReloadRequests() { select { case p.cooldownCh <- struct{}{}: p.l.Info("Starting to reload routes") + nRoutes := p.routes.Size() p.StopAllRoutes() p.loadRoutes() p.StartAllRoutes() - p.l.Info("Routes reloaded") + p.l.Infof("Routes reloaded (%d -> %d)", nRoutes, p.routes.Size()) go func() { time.Sleep(reloadCooldown) @@ -212,4 +213,4 @@ func (p *Provider) loadRoutes() E.NestedError { return errors.Build() } -const reloadCooldown = 300 * time.Millisecond +const reloadCooldown = 50 * time.Millisecond diff --git a/src/utils/functional/map.go b/src/utils/functional/map.go index e1ede2ea..007c13b0 100644 --- a/src/utils/functional/map.go +++ b/src/utils/functional/map.go @@ -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.