From 8a32d3bb8c3b7fd66f5c01086db5f862712b8ed7 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:17:59 +0800 Subject: [PATCH 01/13] feat: support use allow* multiple times in env, flag and docker labels --- README.md | 39 +++++++++++++++- cmd/socket-proxy/handlehttprequest.go | 12 ++++- internal/config/config.go | 65 +++++++++++++++------------ internal/config/env.go | 29 ++++++++++++ internal/config/env_test.go | 49 ++++++++++++++++++++ internal/config/param.go | 36 +++++++++++++++ 6 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 internal/config/env.go create mode 100644 internal/config/env_test.go create mode 100644 internal/config/param.go diff --git a/README.md b/README.md index 5eaa8bf..037d5d6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github > [!NOTE] > Starting with version 1.6.0, the socket-proxy container image is also available on GHCR. +> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels ## Getting Started @@ -93,10 +94,13 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins Examples (command-line): + `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2. + `'-allowHEAD=.*'` allows all HEAD requests. ++ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times Examples (env variables): + `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2. + `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests. ++ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times + For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/). @@ -135,6 +139,8 @@ services: - docker-proxynet # this should be only restricted to traefik and socket-proxy labels: - 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy + - 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy + - 'socket-proxy.allow.post.1=/exec' # another HEAD `exec` requests to socket-proxy ``` When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access. @@ -206,6 +212,35 @@ networks: internal: true ``` +### Example for multiple times `-allow*` + +``` +``` compose.yaml +services: + dockerproxy: + image: wollomatic/socket-proxy:<> # choose most recent image + restart: unless-stopped + user: "65534:<>" + mem_limit: 64M + read_only: true + cap_drop: + - ALL + security_opt: + - no-new-privileges + command: + - '-loglevel=info' + - '-listenip=0.0.0.0' + - '-allowfrom=traefik' # allow only hostname "traefik" to connect + - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' + - '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories + - '-watchdoginterval=3600' # check once per hour for socket availability + - '-stoponwatchdog' # halt program on error and let compose restart it + - '-shutdowngracetime=5' # wait 5 seconds before shutting down + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + +``` + ### Examining the API calls of the client application To log the API calls of the client application, set the log level to `DEBUG` and allow all requests. Then, you can examine the log output to determine which requests the client application makes. Allowing all requests can be done by setting the following parameters: @@ -227,7 +262,7 @@ To log the API calls of the client application, set the log level to `DEBUG` and socket-proxy can be configured via command-line parameters or via environment variables. If both command-line parameters and environment variables are set, the environment variable will be ignored. | Parameter | Environment Variable | Default Value | Description | -|--------------------------------|----------------------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-allowfrom` | `SP_ALLOWFROM` | `127.0.0.1/32` | Specifies the IP addresses or hostnames (comma-separated) of the clients or the hostname of one specific client allowed to connect to the proxy. The default value is `127.0.0.1/32`, which means only localhost is allowed. This default configuration may not be useful in most cases, but it is because of a secure-by-default design. To allow all IPv4 addresses, set `-allowfrom=0.0.0.0/0`. Alternatively, hostnames can be set, for example `-allowfrom=traefik`, or `-allowfrom=traefik,dozzle`. Please remember that socket-proxy should never be exposed to a public network, regardless of this extra security layer. | | `-allowbindmountfrom` | `SP_ALLOWBINDMOUNTFROM` | (not set) | Specifies the directories (comma-separated) that are allowed as bind mount sources. If not set, no bind mount restrictions are applied. When set, only bind mounts from the specified directories or their subdirectories are allowed. Each directory must start with `/`. For example, `-allowbindmountfrom=/home,/var/log` allows bind mounts from `/home`, `/var/log`, and any subdirectories. | | `-allowhealthcheck` | `SP_ALLOWHEALTHCHECK` | (not set/false) | If set, it allows the included health check binary to check the socket connection via TCP port 55555 (socket-proxy then listens on `127.0.0.1:55555/health`) | @@ -235,7 +270,7 @@ socket-proxy can be configured via command-line parameters or via environment va | `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. | | `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. | | `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. | -| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | | +| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | | | `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. | | `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. | | `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) | diff --git a/cmd/socket-proxy/handlehttprequest.go b/cmd/socket-proxy/handlehttprequest.go index 7bf1092..4b02612 100644 --- a/cmd/socket-proxy/handlehttprequest.go +++ b/cmd/socket-proxy/handlehttprequest.go @@ -5,6 +5,7 @@ import ( "log/slog" "net" "net/http" + "regexp" "github.com/wollomatic/socket-proxy/internal/config" ) @@ -24,7 +25,7 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) { communicateBlockedRequest(w, r, "method not allowed", http.StatusMethodNotAllowed) return } - if !allowed.MatchString(r.URL.Path) { // path does not match regex -> not allowed + if !matchURL(allowed, r.URL.Path) { // path does not match regex -> not allowed communicateBlockedRequest(w, r, "path not allowed", http.StatusForbidden) return } @@ -40,6 +41,15 @@ func handleHTTPRequest(w http.ResponseWriter, r *http.Request) { socketProxy.ServeHTTP(w, r) // proxy the request } +func matchURL(allowedURIs []*regexp.Regexp, requestURI string) bool { + for _, allowedURI := range allowedURIs { + if allowedURI.MatchString(requestURI) { + return true + } + } + return false +} + // return the relevant allowlist func determineAllowList(r *http.Request) (config.AllowList, bool) { if cfg.ProxySocketEndpoint == "" { // do not perform this check if we proxy to a unix socket diff --git a/internal/config/config.go b/internal/config/config.go index 9d91f69..2445f96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,22 +67,20 @@ type AllowListRegistry struct { } type AllowList struct { - ID string // Container ID (empty for the default allowlist) - AllowedRequests map[string]*regexp.Regexp // map of request methods to request path regex patterns (no requests allowed if empty) - AllowedBindMounts []string // list of from portion of allowed bind mounts (all bind mounts allowed if empty) + ID string // Container ID (empty for the default allowlist) + AllowedRequests map[string][]*regexp.Regexp // map of request methods to request path regex patterns (no requests allowed if empty) + AllowedBindMounts []string // list of from portion of allowed bind mounts (all bind mounts allowed if empty) } // used for list of allowed requests type methodRegex struct { - method string - regexStringFromEnv string - regexStringFromParam string + method string + regexStrings arrayParams } // mr is the allowlist of requests per http method -// default: regexStringFromEnv and regexStringFromParam are empty, so regexCompiled stays nil and the request is blocked -// if regexStringParam is set with a command line parameter, all requests matching the method and path matching the regex are allowed -// else if regexStringEnv from Environment ist checked +// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked +// if regexStrings is set, all requests matching the method and path matching the regex are allowed var mr = []methodRegex{ {method: http.MethodGet}, {method: http.MethodHead}, @@ -164,8 +162,13 @@ func InitConfig() (*Config, error) { } for i := range mr { - if val, ok := os.LookupEnv("SP_ALLOW_" + mr[i].method); ok && val != "" { - mr[i].regexStringFromEnv = val + // multiple values per method + // like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ... + allowFromEnv := getAllowFromEnv(os.Environ()) + if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 { + for _, v := range val { + mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv}) + } } } @@ -190,7 +193,7 @@ func InitConfig() (*Config, error) { flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)") flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name") for i := range mr { - flag.StringVar(&mr[i].regexStringFromParam, "allow"+mr[i].method, "", "regex for "+mr[i].method+" requests (not set means method is not allowed)") + flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)") } flag.Parse() @@ -245,20 +248,23 @@ func InitConfig() (*Config, error) { cfg.ProxySocketEndpointFileMode = os.FileMode(uint32(endpointFileMode)) // compile regexes for default allowed requests - cfg.AllowLists.Default.AllowedRequests = make(map[string]*regexp.Regexp) + cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp) for _, rx := range mr { - if rx.regexStringFromParam != "" { - r, err := compileRegexp(rx.regexStringFromParam, rx.method, "command line parameter") - if err != nil { - return nil, err - } - cfg.AllowLists.Default.AllowedRequests[rx.method] = r - } else if rx.regexStringFromEnv != "" { - r, err := compileRegexp(rx.regexStringFromEnv, rx.method, "env variable") - if err != nil { - return nil, err + for _, regexString := range rx.regexStrings { + if regexString.value != "" { + location := "" + switch regexString.from { + case fromEnv: + location = "env variable" + case fromParam: + location = "command line parameter" + } + r, err := compileRegexp(regexString.value, rx.method, location) + if err != nil { + return nil, err + } + cfg.AllowLists.Default.AllowedRequests[rx.method] = append(cfg.AllowLists.Default.AllowedRequests[rx.method], r) } - cfg.AllowLists.Default.AllowedRequests[rx.method] = r } } @@ -634,18 +640,21 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont } // extract Docker container allowlist label data from the container summary -func extractLabelData(cntr container.Summary) (map[string]*regexp.Regexp, []string, error) { - allowedRequests := make(map[string]*regexp.Regexp) +func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []string, error) { + allowedRequests := make(map[string][]*regexp.Regexp) var allowedBindMounts []string for labelName, labelValue := range cntr.Labels { if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" { allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix)) - if slices.ContainsFunc(mr, func(rx methodRegex) bool { return rx.method == allowSpec }) { + if slices.ContainsFunc(mr, func(rx methodRegex) bool { + // allowSpec starts with the method name like socket-proxy.allow.get.1 + return strings.HasPrefix(allowSpec, rx.method) + }) { r, err := compileRegexp(labelValue, allowSpec, "docker container label") if err != nil { return nil, nil, err } - allowedRequests[allowSpec] = r + allowedRequests[allowSpec] = append(allowedRequests[allowSpec], r) } else if allowSpec == "BINDMOUNTFROM" { var err error allowedBindMounts, err = parseAllowedBindMounts(labelValue) diff --git a/internal/config/env.go b/internal/config/env.go new file mode 100644 index 0000000..b502af7 --- /dev/null +++ b/internal/config/env.go @@ -0,0 +1,29 @@ +package config + +import ( + "strings" +) + +const sp_allowPrefix = "SP_ALLOW_" + +// getAllowFromEnv reads allowlist regex strings from environment variables. +// +// Environment variables should be of the form +// like SP_ALLOW_GET, SP_ALLOW_GET_0, SP_ALLOW_GET_1, SP_ALLOW_POST +// returning a map of method to list of regex strings. +// like: {"GET":[], "POST":[]} +func getAllowFromEnv(env []string) map[string][]string { + result := make(map[string][]string) + for _, v := range env { + if v, ok := strings.CutPrefix(v, sp_allowPrefix); ok { + key, value, found := strings.Cut(v, "=") + if found { + // optional number suffix after method + method, _, _ := strings.Cut(key, "_") + result[method] = append(result[method], value) + + } + } + } + return result +} diff --git a/internal/config/env_test.go b/internal/config/env_test.go new file mode 100644 index 0000000..5935c51 --- /dev/null +++ b/internal/config/env_test.go @@ -0,0 +1,49 @@ +package config + +import ( + "reflect" + "testing" +) + +func Test_getAllowFromEnv(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + env []string + want map[string][]string + }{ + { + name: "single method", + env: []string{"SP_ALLOW_GET=/allowed/path"}, + want: map[string][]string{"GET": {"/allowed/path"}}, + }, + { + name: "multiple methods", + env: []string{"SP_ALLOW_GET=/get/path", "SP_ALLOW_POST=/post/path"}, + want: map[string][]string{"GET": {"/get/path"}, "POST": {"/post/path"}}, + }, + { + name: "multiple entries for one method", + env: []string{"SP_ALLOW_GET=/path/one", "SP_ALLOW_GET_1=/path/two"}, + want: map[string][]string{"GET": {"/path/one", "/path/two"}}, + }, + { + name: "multiple entries for one method", + env: []string{"SP_ALLOW_GET=/path/one", "SP_ALLOW_GET_2=/path/two"}, + want: map[string][]string{"GET": {"/path/one", "/path/two"}}, + }, + { + name: "no relevant env vars", + env: []string{"OTHER_ENV=some_value"}, + want: map[string][]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getAllowFromEnv(tt.env) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAllowFromEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/config/param.go b/internal/config/param.go new file mode 100644 index 0000000..3408e04 --- /dev/null +++ b/internal/config/param.go @@ -0,0 +1,36 @@ +package config + +import ( + "flag" + "strings" +) + +type from int + +const ( + fromEnv from = 1 + fromParam from = 2 +) + +type param struct { + value string + from from +} + +type arrayParams []param + +// ensure that arrayParams implements the flag.Value interface +var _ flag.Value = (*arrayParams)(nil) + +func (a *arrayParams) String() string { + var values []string + for _, p := range *a { + values = append(values, p.value) + } + return strings.Join(values, ", ") +} + +func (a *arrayParams) Set(value string) error { + *a = append(*a, param{value: value, from: fromParam}) + return nil +} From 8b94aa8073f000de42c12e4b79ef35933c5dbac2 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:34:17 +0800 Subject: [PATCH 02/13] doc: remove useless example --- README.md | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/README.md b/README.md index 037d5d6..082b84f 100644 --- a/README.md +++ b/README.md @@ -212,35 +212,6 @@ networks: internal: true ``` -### Example for multiple times `-allow*` - -``` -``` compose.yaml -services: - dockerproxy: - image: wollomatic/socket-proxy:<> # choose most recent image - restart: unless-stopped - user: "65534:<>" - mem_limit: 64M - read_only: true - cap_drop: - - ALL - security_opt: - - no-new-privileges - command: - - '-loglevel=info' - - '-listenip=0.0.0.0' - - '-allowfrom=traefik' # allow only hostname "traefik" to connect - - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' - - '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories - - '-watchdoginterval=3600' # check once per hour for socket availability - - '-stoponwatchdog' # halt program on error and let compose restart it - - '-shutdowngracetime=5' # wait 5 seconds before shutting down - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - -``` - ### Examining the API calls of the client application To log the API calls of the client application, set the log level to `DEBUG` and allow all requests. Then, you can examine the log output to determine which requests the client application makes. Allowing all requests can be done by setting the following parameters: From e64698d852f85117d9b035fe8a6cd24c853a9195 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:38:53 +0800 Subject: [PATCH 03/13] doc: remove redundant newline --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 082b84f..9780132 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,6 @@ Examples (env variables): + `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests. + `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times - For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/). An excellent online regexp tester is [regex101.com](https://regex101.com/). From 0882d698a255fe8ecf678eae8eed30f9e10876f7 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:40:05 +0800 Subject: [PATCH 04/13] doc: fix typo on docker labels --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9780132..d91bdb1 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ services: labels: - 'socket-proxy.allow.get=.*' # allow all GET requests to socket-proxy - 'socket-proxy.allow.head=/version' # HEAD `/version` requests to socket-proxy - - 'socket-proxy.allow.post.1=/exec' # another HEAD `exec` requests to socket-proxy + - 'socket-proxy.allow.head.1=/exec' # another HEAD `exec` requests to socket-proxy ``` When this is used, it is not necessary to specify the container in `-allowfrom` as the presence of the allowlist labels will grant corresponding access. From 484decb9addb8f55764c24b3d29f8c76934b68bf Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:19:07 +0800 Subject: [PATCH 05/13] fix: docker labels allow* method error --- internal/config/config.go | 6 ++- internal/config/config_test.go | 91 ++++++++++++++++++++++++++++++++++ internal/config/env.go | 1 - 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 internal/config/config_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 2445f96..f4d63e6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -650,11 +650,13 @@ func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []st // allowSpec starts with the method name like socket-proxy.allow.get.1 return strings.HasPrefix(allowSpec, rx.method) }) { - r, err := compileRegexp(labelValue, allowSpec, "docker container label") + // extract the method name from allowSpec + method, _, _ := strings.Cut(allowSpec, ".") + r, err := compileRegexp(labelValue, method, "docker container label") if err != nil { return nil, nil, err } - allowedRequests[allowSpec] = append(allowedRequests[allowSpec], r) + allowedRequests[method] = append(allowedRequests[method], r) } else if allowSpec == "BINDMOUNTFROM" { var err error allowedBindMounts, err = parseAllowedBindMounts(labelValue) diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..4db85c1 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,91 @@ +package config + +import ( + "reflect" + "regexp" + "testing" + + "github.com/wollomatic/socket-proxy/internal/docker/api/types/container" +) + +func Test_extractLabelData(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + cntr container.Summary + want map[string][]*regexp.Regexp + want2 []string + wantErr bool + }{ + { + name: "valid labels with multiple methods and regexes", + cntr: container.Summary{ + Labels: map[string]string{ + "socket-proxy.allow.get.0": "regex1", + "socket-proxy.allow.get.1": "regex2", + "socket-proxy.allow.post": "regex3", + }, + }, + want: map[string][]*regexp.Regexp{ + "GET": {regexp.MustCompile("^regex1$"), regexp.MustCompile("^regex2$")}, + "POST": {regexp.MustCompile("^regex3$")}, + }, + want2: nil, + wantErr: false, + }, + { + name: "invalid regex in label value", + cntr: container.Summary{ + Labels: map[string]string{ + "socket-proxy.allow.get": "invalid[regex", + }, + }, + want: nil, + want2: nil, + wantErr: true, + }, + { + name: "non-allow labels are ignored", + cntr: container.Summary{ + Labels: map[string]string{ + "socket-proxy.allow.get": "regex1", + "other.label": "value", + }, + }, + want: map[string][]*regexp.Regexp{ + "GET": {regexp.MustCompile("^regex1$")}, + }, + }, + { + name: "allow* labels with bindmount", + cntr: container.Summary{ + Labels: map[string]string{ + "socket-proxy.allow.get": "regex1", + }, + }, + want: map[string][]*regexp.Regexp{ + "GET": {regexp.MustCompile("^regex1$")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got2, gotErr := extractLabelData(tt.cntr) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("extractLabelData() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("extractLabelData() succeeded unexpectedly") + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("extractLabelData() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("extractLabelData() = %v, want %v", got2, tt.want2) + } + }) + } +} diff --git a/internal/config/env.go b/internal/config/env.go index b502af7..12ea3dc 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -21,7 +21,6 @@ func getAllowFromEnv(env []string) map[string][]string { // optional number suffix after method method, _, _ := strings.Cut(key, "_") result[method] = append(result[method], value) - } } } From f6fee10a41448f871608b60760a550bbc110d25e Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:33:55 +0800 Subject: [PATCH 06/13] chore: remove redundant test --- internal/config/config_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4db85c1..b489cf2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -56,17 +56,6 @@ func Test_extractLabelData(t *testing.T) { "GET": {regexp.MustCompile("^regex1$")}, }, }, - { - name: "allow* labels with bindmount", - cntr: container.Summary{ - Labels: map[string]string{ - "socket-proxy.allow.get": "regex1", - }, - }, - want: map[string][]*regexp.Regexp{ - "GET": {regexp.MustCompile("^regex1$")}, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 4900b7caa2913ece0605f1df90d0b6a813f0565e Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:06:08 +0800 Subject: [PATCH 07/13] chore(config): move getAllowFromEnv out for loop --- internal/config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index f4d63e6..e6c0a1b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -161,10 +161,10 @@ func InitConfig() (*Config, error) { defaultProxyContainerName = val } + // multiple values per method + // like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ... + allowFromEnv := getAllowFromEnv(os.Environ()) for i := range mr { - // multiple values per method - // like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ... - allowFromEnv := getAllowFromEnv(os.Environ()) if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 { for _, v := range val { mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv}) From a6187d803a6f236859472f541e99b3863b0ea229 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:07:22 +0800 Subject: [PATCH 08/13] test(config): rename test name --- internal/config/env_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/env_test.go b/internal/config/env_test.go index 5935c51..aadb948 100644 --- a/internal/config/env_test.go +++ b/internal/config/env_test.go @@ -28,7 +28,7 @@ func Test_getAllowFromEnv(t *testing.T) { want: map[string][]string{"GET": {"/path/one", "/path/two"}}, }, { - name: "multiple entries for one method", + name: "multiple entries for one method with non-sequential index", env: []string{"SP_ALLOW_GET=/path/one", "SP_ALLOW_GET_2=/path/two"}, want: map[string][]string{"GET": {"/path/one", "/path/two"}}, }, From 381964fb6f943c6dc7e5d75fd4d9b61e287b4a3d Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:11:38 +0800 Subject: [PATCH 09/13] doc: fix markdown table --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d91bdb1..78bac5a 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ socket-proxy can be configured via command-line parameters or via environment va | `-logjson` | `SP_LOGJSON` | (not set/false) | If set, it enables logging in JSON format. If unset, socket-proxy logs in plain text format. | | `-loglevel` | `SP_LOGLEVEL` | `INFO` | Sets the log level. Accepted values are: `DEBUG`, `INFO`, `WARN`, `ERROR`. | | `-proxyport` | `SP_PROXYPORT` | `2375` | Defines the TCP port the proxy listens to. | -| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | | +| `-shutdowngracetime` | `SP_SHUTDOWNGRACETIME` | `10` | Defines the time in seconds to wait before forcing the shutdown after SIGTERM or SIGINT (socket-proxy first tries to gracefully shut down the TCP server) | | `-socketpath` | `SP_SOCKETPATH` | `/var/run/docker.sock` | Specifies the UNIX socket path to connect to. By default, it connects to the Docker daemon socket. | | `-stoponwatchdog` | `SP_STOPONWATCHDOG` | (not set/false) | If set, socket-proxy will be stopped if the watchdog detects that the unix socket is not available. | | `-watchdoginterval` | `SP_WATCHDOGINTERVAL` | `0` | Check for socket availability every x seconds (disable checks, if not set or value is 0) | From caa268a7071f2de106cc4dd54e51ed43152535e0 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:07:27 +0800 Subject: [PATCH 10/13] test(config): replace reflect.DeepEqual with regexMapsEqual for regexp.Regexp compare --- internal/config/config_test.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b489cf2..6119a0b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -69,7 +69,7 @@ func Test_extractLabelData(t *testing.T) { if tt.wantErr { t.Fatal("extractLabelData() succeeded unexpectedly") } - if !reflect.DeepEqual(got, tt.want) { + if !regexMapsEqual(got, tt.want) { t.Errorf("extractLabelData() = %v, want %v", got, tt.want) } if !reflect.DeepEqual(got2, tt.want2) { @@ -78,3 +78,21 @@ func Test_extractLabelData(t *testing.T) { }) } } + +func regexMapsEqual(a, b map[string][]*regexp.Regexp) bool { + if len(a) != len(b) { + return false + } + for method, aRegexes := range a { + bRegexes, ok := b[method] + if !ok || len(aRegexes) != len(bRegexes) { + return false + } + for i, ar := range aRegexes { + if ar.String() != bRegexes[i].String() { + return false + } + } + } + return true +} From c62f9589fd1cf06a5c7decf19484f40c306cdabf Mon Sep 17 00:00:00 2001 From: wollomatic Date: Sat, 18 Apr 2026 19:41:51 +0200 Subject: [PATCH 11/13] prepare 1.12.0 release --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 566d8a8..b06c1bb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # socket-proxy ## Latest image -- `wollomatic/socket-proxy:1.11.3` / `ghcr.io/wollomatic/socket-proxy:1.11.3` +- `wollomatic/socket-proxy:1.12.0` / `ghcr.io/wollomatic/socket-proxy:1.12.0` - `wollomatic/socket-proxy:1` / `ghcr.io/wollomatic/socket-proxy:1` > [!IMPORTANT] @@ -27,7 +27,6 @@ The source code is available on [GitHub: wollomatic/socket-proxy](https://github > [!NOTE] > Starting with version 1.6.0, the socket-proxy container image is also available on GHCR. -> Starting with version todo, the socket-proxy can set multiple times -allow* in params or environment of docker labels ## Getting Started @@ -107,6 +106,9 @@ An excellent online regexp tester is [regex101.com](https://regex101.com/). To determine which HTTP requests your client application uses, you could switch socket-proxy to debug log level and look at the log output while allowing all requests in a secure environment. +> [!NOTE] +> Starting with version 1.12.0, the socket-proxy can set multiple -allow* in params, environment, or docker labels. + #### Setting up bind mount restrictions By default, socket-proxy does not restrict bind mounts. If you want to add an additional layer of security by restricting which directories can be used as bind mount sources, you can use the `-allowbindmountfrom` parameter or the `SP_ALLOWBINDMOUNTFROM` environment variable. @@ -274,6 +276,7 @@ socket-proxy can be configured via command-line parameters or via environment va 1.11 - add per-container allowlists specified by Docker container labels (thanks [@amanda-wee](https://github.com/amanda-wee)) +1.12 - support use of allow* multiple times in env, flag and docker labels (thanks [@qianlongzt](https://github.com/qianlongzt)) ## License From 9a2e50e3924b9817da2d3cea1dd63641ad143533 Mon Sep 17 00:00:00 2001 From: wollomatic Date: Sat, 18 Apr 2026 20:00:42 +0200 Subject: [PATCH 12/13] minor fix in new examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b06c1bb..50d69ba 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ Use Go's regexp syntax to create the patterns for these parameters. To avoid ins Examples (command-line): + `'-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'` could be used for allowing access to the docker socket for Traefik v2. + `'-allowHEAD=.*'` allows all HEAD requests. -+ `'-allowGET=/version' '-allowGET=/_ping'` allow use `GET` multiple times ++ `'-allowGET=/version -allowGET=/_ping'` use allow `GET` multiple times Examples (env variables): + `'SP_ALLOW_GET="/v1\..{1,2}/(version|containers/.*|events.*)"'` could be used for allowing access to the docker socket for Traefik v2. + `'SP_ALLOW_HEAD=".*"'` allows all HEAD requests. -+ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2=/_ping'` allow use `GET` multiple times ++ `'SP_ALLOW_GET="/version" SP_ALLOW_GET_2="/_ping"'` use allow `GET` multiple times For more information, refer to the [Go regexp documentation](https://golang.org/pkg/regexp/syntax/). From 844457f182a7057135ff8564df7b4c5008fb467e Mon Sep 17 00:00:00 2001 From: wollomatic Date: Sun, 26 Apr 2026 18:15:23 +0200 Subject: [PATCH 13/13] refactor `InitConfig` and related methods for improved maintainability and clarity --- internal/config/config.go | 176 ++++++++++++++++++++------------- internal/config/config_test.go | 68 +++++++++++++ 2 files changed, 178 insertions(+), 66 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e6c0a1b..d2b5cda 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,7 +26,7 @@ import ( const allowedDockerLabelPrefix = "socket-proxy.allow." -var ( +const ( defaultAllowFrom = "127.0.0.1/32" // allowed IPs to connect to the proxy defaultAllowHealthcheck = false // allow health check requests (HEAD http://localhost:55555/health) defaultLogJSON = false // if true, log in JSON format @@ -78,122 +78,131 @@ type methodRegex struct { regexStrings arrayParams } -// mr is the allowlist of requests per http method -// default: regexStrings are empty, so regexCompiled stays nil and the request is blocked -// if regexStrings is set, all requests matching the method and path matching the regex are allowed -var mr = []methodRegex{ - {method: http.MethodGet}, - {method: http.MethodHead}, - {method: http.MethodPost}, - {method: http.MethodPut}, - {method: http.MethodPatch}, - {method: http.MethodDelete}, - {method: http.MethodConnect}, - {method: http.MethodTrace}, - {method: http.MethodOptions}, +var supportedHTTPMethods = []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodTrace, + http.MethodOptions, } +// InitConfig reads configuration from environment variables and command-line +// flags, validates the resulting values, and returns the initialized Config. func InitConfig() (*Config, error) { var ( - cfg Config - allowFromString string - listenIP string - proxyPort uint - logLevel string - endpointFileMode uint - allowBindMountFromString string + cfg Config + allowFromString string + listenIP string + proxyPort uint + logLevel string + endpointFileMode uint + allowBindMountFromString string + defaultAllowFromValue = defaultAllowFrom + defaultAllowHealthcheckValue = defaultAllowHealthcheck + defaultLogJSONValue = defaultLogJSON + defaultListenIPValue = defaultListenIP + defaultLogLevelValue = defaultLogLevel + defaultProxyPortValue = defaultProxyPort + defaultShutdownGraceTimeValue = defaultShutdownGraceTime + defaultSocketPathValue = defaultSocketPath + defaultStopOnWatchdogValue = defaultStopOnWatchdog + defaultWatchdogIntervalValue = defaultWatchdogInterval + defaultProxySocketEndpointValue = defaultProxySocketEndpoint + defaultProxySocketEndpointFileModeValue = defaultProxySocketEndpointFileMode + defaultAllowBindMountFromValue = defaultAllowBindMountFrom + defaultProxyContainerNameValue = defaultProxyContainerName ) if val, ok := os.LookupEnv("SP_ALLOWFROM"); ok && val != "" { - defaultAllowFrom = val + defaultAllowFromValue = val } if val, ok := os.LookupEnv("SP_ALLOWHEALTHCHECK"); ok { if parsedVal, err := strconv.ParseBool(val); err == nil { - defaultAllowHealthcheck = parsedVal + defaultAllowHealthcheckValue = parsedVal } } if val, ok := os.LookupEnv("SP_LOGJSON"); ok { if parsedVal, err := strconv.ParseBool(val); err == nil { - defaultLogJSON = parsedVal + defaultLogJSONValue = parsedVal } } if val, ok := os.LookupEnv("SP_LISTENIP"); ok && val != "" { - defaultListenIP = val + defaultListenIPValue = val } if val, ok := os.LookupEnv("SP_LOGLEVEL"); ok && val != "" { - defaultLogLevel = val + defaultLogLevelValue = val } if val, ok := os.LookupEnv("SP_PROXYPORT"); ok && val != "" { if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { - defaultProxyPort = uint(parsedVal) + defaultProxyPortValue = uint(parsedVal) } } if val, ok := os.LookupEnv("SP_SHUTDOWNGRACETIME"); ok && val != "" { if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { - defaultShutdownGraceTime = uint(parsedVal) + defaultShutdownGraceTimeValue = uint(parsedVal) } } if val, ok := os.LookupEnv("SP_SOCKETPATH"); ok && val != "" { - defaultSocketPath = val + defaultSocketPathValue = val } if val, ok := os.LookupEnv("SP_STOPONWATCHDOG"); ok { if parsedVal, err := strconv.ParseBool(val); err == nil { - defaultStopOnWatchdog = parsedVal + defaultStopOnWatchdogValue = parsedVal } } if val, ok := os.LookupEnv("SP_WATCHDOGINTERVAL"); ok && val != "" { if parsedVal, err := strconv.ParseUint(val, 10, 32); err == nil { - defaultWatchdogInterval = uint(parsedVal) + defaultWatchdogIntervalValue = uint(parsedVal) } } if val, ok := os.LookupEnv("SP_PROXYSOCKETENDPOINT"); ok && val != "" { - defaultProxySocketEndpoint = val + defaultProxySocketEndpointValue = val } if val, ok := os.LookupEnv("SP_PROXYSOCKETENDPOINTFILEMODE"); ok { if parsedVal, err := strconv.ParseUint(val, 8, 32); err == nil { - defaultProxySocketEndpointFileMode = uint(parsedVal) + defaultProxySocketEndpointFileModeValue = uint(parsedVal) } } if val, ok := os.LookupEnv("SP_ALLOWBINDMOUNTFROM"); ok && val != "" { - defaultAllowBindMountFrom = val + defaultAllowBindMountFromValue = val } if val, ok := os.LookupEnv("SP_PROXYCONTAINERNAME"); ok && val != "" { - defaultProxyContainerName = val + defaultProxyContainerNameValue = val } + methodAllowLists := newMethodRegexes() + // multiple values per method // like SP_ALLOW_GET_0, SP_ALLOW_GET_1, ... allowFromEnv := getAllowFromEnv(os.Environ()) - for i := range mr { - if val, ok := allowFromEnv[mr[i].method]; ok && len(val) > 0 { + for i := range methodAllowLists { + if val, ok := allowFromEnv[methodAllowLists[i].method]; ok && len(val) > 0 { for _, v := range val { - mr[i].regexStrings = append(mr[i].regexStrings, param{value: v, from: fromEnv}) + methodAllowLists[i].regexStrings = append(methodAllowLists[i].regexStrings, param{value: v, from: fromEnv}) } } } - flag.StringVar(&allowFromString, "allowfrom", defaultAllowFrom, "allowed IPs or hostname to connect to the proxy") - flag.BoolVar(&cfg.AllowHealthcheck, "allowhealthcheck", defaultAllowHealthcheck, "allow health check requests (HEAD http://localhost:55555/health)") - flag.BoolVar(&cfg.LogJSON, "logjson", defaultLogJSON, "log in JSON format (otherwise log in plain text") - flag.StringVar(&listenIP, "listenip", defaultListenIP, "ip address to listen on") - flag.StringVar(&logLevel, "loglevel", defaultLogLevel, "set log level: DEBUG, INFO, WARN, ERROR") - flag.UintVar(&proxyPort, "proxyport", defaultProxyPort, "tcp port to listen on") - flag.UintVar(&cfg.ShutdownGraceTime, "shutdowngracetime", defaultShutdownGraceTime, "maximum time in seconds to wait for the server to shut down gracefully") - if cfg.ShutdownGraceTime > math.MaxInt { - return nil, fmt.Errorf("shutdowngracetime has to be smaller than %d", math.MaxInt) // this maximum value has no practical significance - } - flag.StringVar(&cfg.SocketPath, "socketpath", defaultSocketPath, "unix socket path to connect to") - flag.BoolVar(&cfg.StopOnWatchdog, "stoponwatchdog", defaultStopOnWatchdog, "stop the program when the socket gets unavailable (otherwise log only)") - flag.UintVar(&cfg.WatchdogInterval, "watchdoginterval", defaultWatchdogInterval, "watchdog interval in seconds (0 to disable)") - if cfg.WatchdogInterval > math.MaxInt { - return nil, fmt.Errorf("watchdoginterval has to be smaller than %d", math.MaxInt) // this maximum value has no practical significance - } - flag.StringVar(&cfg.ProxySocketEndpoint, "proxysocketendpoint", defaultProxySocketEndpoint, "unix socket endpoint (if set, used instead of the TCP listener)") - flag.UintVar(&endpointFileMode, "proxysocketendpointfilemode", defaultProxySocketEndpointFileMode, "set the file mode of the unix socket endpoint") - flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFrom, "allowed directories for bind mounts (comma-separated)") - flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerName, "socket-proxy Docker container name") - for i := range mr { - flag.Var(&mr[i].regexStrings, "allow"+mr[i].method, "regex for "+mr[i].method+" requests (not set means method is not allowed)") + flag.StringVar(&allowFromString, "allowfrom", defaultAllowFromValue, "allowed IPs or hostname to connect to the proxy") + flag.BoolVar(&cfg.AllowHealthcheck, "allowhealthcheck", defaultAllowHealthcheckValue, "allow health check requests (HEAD http://localhost:55555/health)") + flag.BoolVar(&cfg.LogJSON, "logjson", defaultLogJSONValue, "log in JSON format (otherwise log in plain text") + flag.StringVar(&listenIP, "listenip", defaultListenIPValue, "ip address to listen on") + flag.StringVar(&logLevel, "loglevel", defaultLogLevelValue, "set log level: DEBUG, INFO, WARN, ERROR") + flag.UintVar(&proxyPort, "proxyport", defaultProxyPortValue, "tcp port to listen on") + flag.UintVar(&cfg.ShutdownGraceTime, "shutdowngracetime", defaultShutdownGraceTimeValue, "maximum time in seconds to wait for the server to shut down gracefully") + flag.StringVar(&cfg.SocketPath, "socketpath", defaultSocketPathValue, "unix socket path to connect to") + flag.BoolVar(&cfg.StopOnWatchdog, "stoponwatchdog", defaultStopOnWatchdogValue, "stop the program when the socket gets unavailable (otherwise log only)") + flag.UintVar(&cfg.WatchdogInterval, "watchdoginterval", defaultWatchdogIntervalValue, "watchdog interval in seconds (0 to disable)") + flag.StringVar(&cfg.ProxySocketEndpoint, "proxysocketendpoint", defaultProxySocketEndpointValue, "unix socket endpoint (if set, used instead of the TCP listener)") + flag.UintVar(&endpointFileMode, "proxysocketendpointfilemode", defaultProxySocketEndpointFileModeValue, "set the file mode of the unix socket endpoint") + flag.StringVar(&allowBindMountFromString, "allowbindmountfrom", defaultAllowBindMountFromValue, "allowed directories for bind mounts (comma-separated)") + flag.StringVar(&cfg.ProxyContainerName, "proxycontainername", defaultProxyContainerNameValue, "socket-proxy Docker container name") + for i := range methodAllowLists { + flag.Var(&methodAllowLists[i].regexStrings, "allow"+methodAllowLists[i].method, "regex for "+methodAllowLists[i].method+" requests (not set means method is not allowed)") } flag.Parse() @@ -216,6 +225,12 @@ func InitConfig() (*Config, error) { if proxyPort < 1 || proxyPort > 65535 { return nil, errors.New("port number has to be between 1 and 65535") } + if cfg.ShutdownGraceTime > math.MaxInt { + return nil, fmt.Errorf("shutdowngracetime has to be smaller than %d", math.MaxInt) // this maximum value has no practical significance + } + if cfg.WatchdogInterval > math.MaxInt { + return nil, fmt.Errorf("watchdoginterval has to be smaller than %d", math.MaxInt) // this maximum value has no practical significance + } ip := net.ParseIP(listenIP) if ip == nil { return nil, fmt.Errorf("invalid IP \"%s\" for listenip", listenIP) @@ -249,8 +264,8 @@ func InitConfig() (*Config, error) { // compile regexes for default allowed requests cfg.AllowLists.Default.AllowedRequests = make(map[string][]*regexp.Regexp) - for _, rx := range mr { - for _, regexString := range rx.regexStrings { + for _, rx := range methodAllowLists { + for _, regexString := range effectiveMethodParams(rx.regexStrings) { if regexString.value != "" { location := "" switch regexString.from { @@ -293,7 +308,12 @@ func (cfg *Config) UpdateAllowLists() { slog.Error("failed to create Docker client", "error", err) return } - defer dockerClient.Close() + defer func(dockerClient *client.Client) { + err := dockerClient.Close() + if err != nil { + slog.Error("failed to close Docker client", "error", err) + } + }(dockerClient) err = cfg.AllowLists.initByIP(ctx, dockerClient) if err != nil { @@ -581,6 +601,25 @@ func compileRegexp(regex, method, configLocation string) (*regexp.Regexp, error) return r, nil } +// newMethodRegexes returns one methodRegex entry for each supported HTTP method. +func newMethodRegexes() []methodRegex { + methods := make([]methodRegex, 0, len(supportedHTTPMethods)) + for _, method := range supportedHTTPMethods { + methods = append(methods, methodRegex{method: method}) + } + return methods +} + +// effectiveMethodParams returns the parameters that should be applied for one +// HTTP method, preferring command-line values over environment values when both +// are present. +func effectiveMethodParams(params arrayParams) []param { + if slices.ContainsFunc(params, func(p param) bool { return p.from == fromParam }) { + return slices.DeleteFunc(slices.Clone(params), func(p param) bool { return p.from == fromEnv }) + } + return params +} + // parse bind mount from string into list of allowed bind mounts func parseAllowedBindMounts(allowBindMountFromString string) ([]string, error) { allowedBindMounts := strings.Split(allowBindMountFromString, ",") @@ -618,7 +657,12 @@ func getSocketProxyContainerSummary(socketPath, proxyContainerName string) (cont if err != nil { return container.Summary{}, err } - defer dockerClient.Close() + defer func(dockerClient *client.Client) { + err := dockerClient.Close() + if err != nil { + slog.Error("failed to close Docker client", "error", err) + } + }(dockerClient) ctx := context.Background() filter := filters.NewArgs() @@ -646,9 +690,9 @@ func extractLabelData(cntr container.Summary) (map[string][]*regexp.Regexp, []st for labelName, labelValue := range cntr.Labels { if strings.HasPrefix(labelName, allowedDockerLabelPrefix) && labelValue != "" { allowSpec := strings.ToUpper(strings.TrimPrefix(labelName, allowedDockerLabelPrefix)) - if slices.ContainsFunc(mr, func(rx methodRegex) bool { + if slices.ContainsFunc(supportedHTTPMethods, func(method string) bool { // allowSpec starts with the method name like socket-proxy.allow.get.1 - return strings.HasPrefix(allowSpec, rx.method) + return strings.HasPrefix(allowSpec, method) }) { // extract the method name from allowSpec method, _, _ := strings.Cut(allowSpec, ".") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6119a0b..0067399 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,13 +1,33 @@ package config import ( + "flag" + "math" + "os" "reflect" "regexp" + "strconv" "testing" "github.com/wollomatic/socket-proxy/internal/docker/api/types/container" ) +func resetFlagsForTest(t *testing.T, args []string) func() { + t.Helper() + + prevCommandLine := flag.CommandLine + prevArgs := os.Args + + flag.CommandLine = flag.NewFlagSet(args[0], flag.ContinueOnError) + flag.CommandLine.SetOutput(os.Stderr) + os.Args = args + + return func() { + flag.CommandLine = prevCommandLine + os.Args = prevArgs + } +} + func Test_extractLabelData(t *testing.T) { tests := []struct { name string // description of this test case @@ -96,3 +116,51 @@ func regexMapsEqual(a, b map[string][]*regexp.Regexp) bool { } return true } + +func TestInitConfig_AllowMethodFlagOverridesEnv(t *testing.T) { + t.Setenv("SP_ALLOW_GET", "/from-env") + restore := resetFlagsForTest(t, []string{"socket-proxy", "-allowGET=/from-flag"}) + defer restore() + + cfg, err := InitConfig() + if err != nil { + t.Fatalf("InitConfig() error = %v", err) + } + + regexes := cfg.AllowLists.Default.AllowedRequests["GET"] + if len(regexes) != 1 { + t.Fatalf("expected 1 GET regex, got %d", len(regexes)) + } + if !regexes[0].MatchString("/from-flag") { + t.Fatalf("expected GET regex to match /from-flag, got %q", regexes[0].String()) + } + if regexes[0].MatchString("/from-env") { + t.Fatalf("expected env GET regex to be ignored when flag is present, got %q", regexes[0].String()) + } +} + +func TestInitConfig_ShutdownGraceTimeTooLarge(t *testing.T) { + restore := resetFlagsForTest(t, []string{ + "socket-proxy", + "-shutdowngracetime=" + strconv.FormatUint(uint64(math.MaxInt)+1, 10), + }) + defer restore() + + _, err := InitConfig() + if err == nil { + t.Fatal("InitConfig() unexpectedly succeeded") + } +} + +func TestInitConfig_WatchdogIntervalTooLarge(t *testing.T) { + restore := resetFlagsForTest(t, []string{ + "socket-proxy", + "-watchdoginterval=" + strconv.FormatUint(uint64(math.MaxInt)+1, 10), + }) + defer restore() + + _, err := InitConfig() + if err == nil { + t.Fatal("InitConfig() unexpectedly succeeded") + } +}