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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 +}